Merge branch 'feature/File_Attachment' into develop #189

This commit is contained in:
J-Jamet
2020-08-27 19:14:51 +02:00
78 changed files with 1546 additions and 706 deletions

View File

@@ -1,4 +1,5 @@
KeePassDX(2.8.3) KeePassDX(2.8.3)
* Add attachments
KeePassDX(2.8.2) KeePassDX(2.8.2)
* Fix themes / new UI * Fix themes / new UI

View File

@@ -45,8 +45,9 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
@@ -86,7 +87,7 @@ class EntryActivity : LockingActivity() {
private var mShowPassword: Boolean = false private var mShowPassword: Boolean = false
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap() private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false private var mFirstLaunchOfActivity: Boolean = false
@@ -212,8 +213,8 @@ class EntryActivity : LockingActivity() {
mAttachmentFileBinderManager?.apply { mAttachmentFileBinderManager?.apply {
registerProgressTask() registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
entryContentsView?.updateAttachmentDownloadProgress(attachment) entryContentsView?.putAttachment(entryAttachmentState)
} }
} }
} }
@@ -332,15 +333,10 @@ class EntryActivity : LockingActivity() {
entryContentsView?.setHiddenProtectedValue(!mShowPassword) entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments // Manage attachments
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem -> mDatabase?.binaryPool?.let { binaryPool ->
when (attachmentItem.downloadState) { entryContentsView?.assignAttachments(entry.getAttachments(binaryPool), StreamDirection.DOWNLOAD) { attachmentItem ->
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> { createDocument(this, attachmentItem.name)?.let { requestCode ->
createDocument(this, attachmentItem.name)?.let { requestCode -> mAttachmentsToDownload[requestCode] = attachmentItem
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
} }
} }
} }

View File

@@ -22,6 +22,8 @@ import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.TimePickerDialog import android.app.TimePickerDialog
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
@@ -36,18 +38,18 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.* import com.kunzisoft.keepass.activities.dialogs.*
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.model.FocusedEditField import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService 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_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
@@ -55,8 +57,10 @@ import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.EntryEditContentsView import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.showActionError import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft import com.kunzisoft.keepass.view.updateLockPaddingLeft
@@ -69,7 +73,9 @@ class EntryEditActivity : LockingActivity(),
GeneratePasswordDialogFragment.GeneratePasswordListener, GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener, SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener, DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener { TimePickerDialog.OnTimeSetListener,
FileTooBigDialogFragment.ActionChooseListener,
ReplaceFileDialogFragment.ActionChooseListener {
private var mDatabase: Database? = null private var mDatabase: Database? = null
@@ -90,6 +96,11 @@ class EntryEditActivity : LockingActivity(),
private var mFocusedEditExtraField: FocusedEditField? = null private var mFocusedEditExtraField: FocusedEditField? = null
// To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
// Education // Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -221,10 +232,16 @@ class EntryEditActivity : LockingActivity(),
isVisible = allowCustomField isVisible = allowCustomField
} }
// Attachment not compatible below KitKat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
menu.findItem(R.id.menu_add_attachment).isVisible = false
}
menu.findItem(R.id.menu_add_otp).apply { menu.findItem(R.id.menu_add_otp).apply {
val allowOTP = mDatabase?.allowOTP == true val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP isEnabled = allowOTP
isVisible = allowOTP // OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
} }
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
@@ -233,6 +250,10 @@ class EntryEditActivity : LockingActivity(),
addNewCustomField() addNewCustomField()
true true
} }
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> { R.id.menu_add_otp -> {
setupOTP() setupOTP()
true true
@@ -242,6 +263,10 @@ class EntryEditActivity : LockingActivity(),
} }
} }
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button // Save button
validateButton = findViewById(R.id.entry_edit_validate) validateButton = findViewById(R.id.entry_edit_validate)
validateButton?.setOnClickListener { saveEntry() } validateButton?.setOnClickListener { saveEntry() }
@@ -273,6 +298,41 @@ class EntryEditActivity : LockingActivity(),
// Padding if lock button visible // Padding if lock button visible
entryEditAddToolBar?.updateLockPaddingLeft() entryEditAddToolBar?.updateLockPaddingLeft()
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
when (entryAttachmentState.downloadState) {
AttachmentState.START -> {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
entryEditContentsView?.clearAttachments()
}
entryEditContentsView?.putAttachment(entryAttachmentState)
}
AttachmentState.IN_PROGRESS -> {
entryEditContentsView?.putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
entryEditContentsView?.putAttachment(entryAttachmentState)
}
AttachmentState.ERROR -> {
mDatabase?.removeAttachmentIfNotUsed(entryAttachmentState.attachment)
entryEditContentsView?.removeAttachment(entryAttachmentState)
}
else -> {}
}
}
}
}
}
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
super.onPause()
} }
private fun populateViewsWithEntry(newEntry: Entry) { private fun populateViewsWithEntry(newEntry: Entry) {
@@ -298,8 +358,11 @@ class EntryEditActivity : LockingActivity(),
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) { assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
Field(it.key, it.value) Field(it.key, it.value)
}, mFocusedEditExtraField) }, mFocusedEditExtraField)
assignAttachments(newEntry.getAttachments()) { attachment ->
newEntry.removeAttachment(attachment) mDatabase?.binaryPool?.let { binaryPool ->
assignAttachments(newEntry.getAttachments(binaryPool), StreamDirection.UPLOAD) { attachment ->
newEntry.removeAttachment(attachment)
}
} }
} }
} }
@@ -321,9 +384,14 @@ class EntryEditActivity : LockingActivity(),
expiryTime = entryView.expiresDate expiryTime = entryView.expiresDate
} }
notes = entryView.notes notes = entryView.notes
entryView.getExtraField().forEach { customField -> entryView.getExtraFields().forEach { customField ->
putExtraField(customField.name, customField.protectedValue) putExtraField(customField.name, customField.protectedValue)
} }
mDatabase?.binaryPool?.let { binaryPool ->
entryView.getAttachments().forEach {
putAttachment(it, binaryPool)
}
}
mFocusedEditExtraField = entryView.getExtraFieldFocused() mFocusedEditExtraField = entryView.getExtraFieldFocused()
} }
} }
@@ -358,6 +426,63 @@ class EntryEditActivity : LockingActivity(),
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {} override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
/**
* Add a new attachment
*/
private fun addNewAttachment(item: MenuItem) {
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
}
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
if (attachmentToUploadUri != null && fileName != null) {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
if (attachmentToUploadUri != null && attachment != null) {
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
}
}
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
val compression = mDatabase?.compressionForNewEntry() ?: false
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
val entryAttachment = Attachment(fileName, binaryAttachment)
// Ask to replace the current attachment
if ((mDatabase?.allowMultipleAttachments != true && entryEditContentsView?.containsAttachment() == true) ||
entryEditContentsView?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
.show(supportFragmentManager, "replacementFileFragment")
} else {
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, entryAttachment)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { attachmentToUploadUri ->
// TODO Async to get the name
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
documentFile.name?.let { fileName ->
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
.show(supportFragmentManager, "fileTooBigFragment")
} else {
buildNewAttachment(attachmentToUploadUri, fileName)
}
}
}
}
}
}
/**
* Set up OTP (HOTP or TOTP) and add it as extra field
*/
private fun setupOTP() { private fun setupOTP() {
// Retrieve the current otpElement if exists // Retrieve the current otpElement if exists
// and open the dialog to set up the OTP // and open the dialog to set up the OTP

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -82,7 +82,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null private var mDatabaseFileUri: Uri? = null
private var mOpenFileHelper: OpenFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
@@ -108,10 +108,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
createDatabaseButtonView?.setOnClickListener { createNewFile() } createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button // Open database button
mOpenFileHelper = OpenFileHelper(this) mSelectFileHelper = SelectFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button) openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply { openDatabaseButtonView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let { mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it) setOnClickListener(it)
setOnLongClickListener(it) setOnLongClickListener(it)
} }
@@ -389,7 +389,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data) AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
} }
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri -> mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) { if (uri != null) {
launchPasswordActivityWithPath(uri) launchPasswordActivityWithPath(uri)
} }
@@ -445,7 +445,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView!!, openDatabaseButtonView!!,
{tapTargetView -> {tapTargetView ->
tapTargetView?.let { tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it) mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
} }
}, },
{} {}

View File

@@ -44,8 +44,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
@@ -64,7 +64,9 @@ import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Compa
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.MASTER_PASSWORD_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.notifications.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.AdvancedUnlockInfoView import com.kunzisoft.keepass.view.AdvancedUnlockInfoView
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
@@ -92,7 +94,7 @@ open class PasswordActivity : SpecialModeActivity() {
private var mDatabaseKeyFileUri: Uri? = null private var mDatabaseKeyFileUri: Uri? = null
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mOpenFileHelper: OpenFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private var mPermissionAsked = false private var mPermissionAsked = false
private var readOnly: Boolean = false private var readOnly: Boolean = false
@@ -136,9 +138,9 @@ open class PasswordActivity : SpecialModeActivity() {
readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState)
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mOpenFileHelper = OpenFileHelper(this@PasswordActivity) mSelectFileHelper = SelectFileHelper(this@PasswordActivity)
keyFileSelectionView?.apply { keyFileSelectionView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let { mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it) setOnClickListener(it)
setOnLongClickListener(it) setOnLongClickListener(it)
} }
@@ -747,7 +749,7 @@ open class PasswordActivity : SpecialModeActivity() {
} }
var keyFileResult = false var keyFileResult = false
mOpenFileHelper?.let { mSelectFileHelper?.let {
keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data keyFileResult = it.onActivityResultCallback(requestCode, resultCode, data
) { uri -> ) { uri ->
if (uri != null) { if (uri != null) {

View File

@@ -25,16 +25,16 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import com.google.android.material.textfield.TextInputLayout
import androidx.fragment.app.DialogFragment
import androidx.appcompat.app.AlertDialog
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.View import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() { class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var mListener: AssignPasswordDialogListener? = null private var mListener: AssignPasswordDialogListener? = null
private var mOpenFileHelper: OpenFileHelper? = null private var mSelectFileHelper: SelectFileHelper? = null
private val passwordTextWatcher = object : TextWatcher { private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
@@ -113,10 +113,10 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mOpenFileHelper = OpenFileHelper(this) mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply { keyFileSelectionView?.apply {
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener) setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener) setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
} }
val dialog = builder.create() val dialog = builder.create()
@@ -249,8 +249,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
) { uri ->
uri?.let { pathUri -> uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri keyFileSelectionView?.uri = pathUri

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
/**
* Custom Dialog to confirm big file to upload
*/
class FileTooBigDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_file_too_big))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mActionChooseListener?.onValidateUploadFileTooBig(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getString(KEY_FILE_NAME))
}
builder.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?)
}
companion object {
const val MAX_WARNING_BINARY_FILE = 5242880
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_FILE_NAME = "KEY_FILE_NAME"
fun build(attachmentToUploadUri: Uri,
fileName: String): FileTooBigDialogFragment {
val fragment = FileTooBigDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putString(KEY_FILE_NAME, fileName)
}
return fragment
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
/**
* Custom Dialog to confirm big file to upload
*/
class ReplaceFileDialogFragment : DialogFragment() {
private var mActionChooseListener: ActionChooseListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
mActionChooseListener = context as ActionChooseListener
} catch (e: ClassCastException) {
throw ClassCastException(context.toString()
+ " must implement " + ActionChooseListener::class.java.name)
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity ->
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(activity)
builder.setMessage(SpannableStringBuilder().apply {
append(getString(R.string.warning_replace_file))
append("\n\n")
append(getString(R.string.warning_sure_add_file))
})
builder.setPositiveButton(android.R.string.yes) { _, _ ->
mActionChooseListener?.onValidateReplaceFile(
arguments?.getParcelable(KEY_FILE_URI),
arguments?.getParcelable(KEY_ENTRY_ATTACHMENT))
}
builder.setNegativeButton(android.R.string.no) { _, _ ->
dismiss()
}
// Create the AlertDialog object and return it
return builder.create()
}
return super.onCreateDialog(savedInstanceState)
}
interface ActionChooseListener {
fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?)
}
companion object {
private const val KEY_FILE_URI = "KEY_FILE_URI"
private const val KEY_ENTRY_ATTACHMENT = "KEY_ENTRY_ATTACHMENT"
fun build(attachmentToUploadUri: Uri,
attachment: Attachment): ReplaceFileDialogFragment {
val fragment = ReplaceFileDialogFragment()
fragment.arguments = Bundle().apply {
putParcelable(KEY_FILE_URI, attachmentToUploadUri)
putParcelable(KEY_ENTRY_ATTACHMENT, attachment)
}
return fragment
}
}
}

View File

@@ -28,19 +28,20 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
class OpenFileHelper { class SelectFileHelper {
private var activity: Activity? = null private var activity: Activity? = null
private var fragment: Fragment? = null private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = OpenFileOnClickViewListener() get() = SelectFileOnClickViewListener()
constructor(context: Activity) { constructor(context: Activity) {
this.activity = context this.activity = context
@@ -52,7 +53,10 @@ class OpenFileHelper {
this.fragment = context this.fragment = context
} }
inner class OpenFileOnClickViewListener : View.OnClickListener, View.OnLongClickListener { inner class SelectFileOnClickViewListener :
View.OnClickListener,
View.OnLongClickListener,
MenuItem.OnMenuItemClickListener {
private fun onAbstractClick(longClick: Boolean = false) { private fun onAbstractClick(longClick: Boolean = false) {
try { try {
@@ -85,6 +89,11 @@ class OpenFileHelper {
onAbstractClick(true) onAbstractClick(true)
return true return true
} }
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")

View File

@@ -32,6 +32,14 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size) onListSizeChangedListener?.invoke(previousSize, itemsList.size)
} }
open fun isEmpty(): Boolean {
return itemsList.isEmpty()
}
open fun contains(item: Item): Boolean {
return itemsList.contains(item)
}
open fun putItem(item: Item) { open fun putItem(item: Item) {
val previousSize = itemsList.size val previousSize = itemsList.size
if (itemsList.contains(item)) { if (itemsList.contains(item)) {
@@ -46,6 +54,13 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size) onListSizeChangedListener?.invoke(previousSize, itemsList.size)
} }
open fun removeItem(item: Item) {
if (itemsList.contains(item)) {
mItemToRemove = item
notifyItemChanged(itemsList.indexOf(item))
}
}
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) { fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
deleteButton.apply { deleteButton.apply {
visibility = View.VISIBLE visibility = View.VISIBLE

View File

@@ -23,36 +23,34 @@ import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boolean) class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachment, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) { : AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var onItemClickListener: ((item: EntryAttachment)->Unit)? = null var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
private val mDatabase = Database.getInstance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false)) return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
} }
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) { override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = itemsList[position] val entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.binaryFileTitle.text = entryAttachment.name holder.binaryFileTitle.text = entryAttachmentState.attachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context, holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length()) entryAttachmentState.attachment.binaryAttachment.length())
holder.binaryFileCompression.apply { holder.binaryFileCompression.apply {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
|| entryAttachment.binaryAttachment.isCompressed == true) {
text = CompressionAlgorithm.GZip.getName(context.resources) text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE visibility = View.VISIBLE
} else { } else {
@@ -60,42 +58,60 @@ class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boole
visibility = View.GONE visibility = View.GONE
} }
} }
if (editable) { when (entryAttachmentState.streamDirection) {
holder.binaryFileProgressContainer.visibility = View.GONE StreamDirection.UPLOAD -> {
holder.binaryFileDeleteButton.apply { holder.binaryFileProgressIcon.isActivated = true
visibility = View.VISIBLE when (entryAttachmentState.downloadState) {
onBindDeleteButton(holder, this, entryAttachment, position) AttachmentState.START,
} AttachmentState.IN_PROGRESS -> {
} else { holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileProgressContainer.visibility = View.VISIBLE holder.binaryFileProgress.apply {
holder.binaryFileDeleteButton.visibility = View.GONE visibility = View.VISIBLE
holder.binaryFileProgress.apply { progress = entryAttachmentState.downloadProgression
visibility = when (entryAttachment.downloadState) { }
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE holder.binaryFileDeleteButton.apply {
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE visibility = View.GONE
setOnClickListener(null)
}
}
AttachmentState.NULL,
AttachmentState.ERROR,
AttachmentState.COMPLETE -> {
holder.binaryFileProgressContainer.visibility = View.GONE
holder.binaryFileProgress.visibility = View.GONE
holder.binaryFileDeleteButton.apply {
visibility = View.VISIBLE
onBindDeleteButton(holder, this, entryAttachmentState, position)
}
}
} }
progress = entryAttachment.downloadProgression holder.itemView.setOnClickListener(null)
} }
holder.itemView.setOnClickListener { StreamDirection.DOWNLOAD -> {
onItemClickListener?.invoke(entryAttachment) holder.binaryFileProgressIcon.isActivated = false
holder.binaryFileProgressContainer.visibility = View.VISIBLE
holder.binaryFileDeleteButton.visibility = View.GONE
holder.binaryFileProgress.apply {
visibility = when (entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
}
progress = entryAttachmentState.downloadProgression
}
holder.itemView.setOnClickListener {
onItemClickListener?.invoke(entryAttachmentState)
}
} }
} }
} }
fun updateProgress(entryAttachment: EntryAttachment) { class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val indexEntryAttachment = itemsList.indexOfLast { current -> current.name == entryAttachment.name }
if (indexEntryAttachment != -1) {
itemsList[indexEntryAttachment] = entryAttachment
notifyItemChanged(indexEntryAttachment)
}
}
inner class EntryBinariesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title) var binaryFileTitle: TextView = itemView.findViewById(R.id.item_attachment_title)
var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size) var binaryFileSize: TextView = itemView.findViewById(R.id.item_attachment_size)
var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression) var binaryFileCompression: TextView = itemView.findViewById(R.id.item_attachment_compression)
var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container) var binaryFileProgressContainer: View = itemView.findViewById(R.id.item_attachment_progress_container)
var binaryFileProgressIcon: ImageView = itemView.findViewById(R.id.item_attachment_icon)
var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress) var binaryFileProgress: ProgressBar = itemView.findViewById(R.id.item_attachment_progress)
var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button) var binaryFileDeleteButton: View = itemView.findViewById(R.id.item_attachment_delete_button)
} }

View File

@@ -338,6 +338,9 @@ class NodeAdapter (private val context: Context)
} }
} }
holder.attachmentIcon?.visibility =
if (entry.containsAttachment()) View.VISIBLE else View.GONE
mDatabase.stopManageEntry(entry) mDatabase.stopManageEntry(entry)
} }
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
var text: TextView = itemView.findViewById(R.id.node_text) var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext) var subText: TextView = itemView.findViewById(R.id.node_subtext)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
} }
companion object { companion object {

View File

@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
} else { } else {
database.deleteEntry(currentNode) database.deleteEntry(currentNode)
} }
// Remove the oldest attachments
currentNode.getAttachments(database.binaryPool).forEach {
database.removeAttachmentIfNotUsed(it)
}
} }
} }
} }

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.action.node package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
// WARNING : Re attribute parent removed in entry edit activity to save memory // WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry) mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool)
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
// Not use equals because only check name
newEntryAttachments.forEach { newAttachment ->
oldEntryAttachments.forEach { oldAttachment ->
if (oldAttachment.name == newAttachment.name
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
attachmentsToRemove.remove(oldAttachment)
}
}
// Update entry with new values // Update entry with new values
mOldEntry.updateWith(mNewEntry) mOldEntry.updateWith(mNewEntry)
mNewEntry.touch(modified = true, touchParents = true) mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history) // Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false)) mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry) database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
// Only change data in index // Only change data in index
database.updateEntry(mOldEntry) database.updateEntry(mOldEntry)
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
} }
override fun nodeFinish(): ActionNodesValues { override fun nodeFinish(): ActionNodesValues {

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags)
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "$name at $binaryAttachment"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Attachment) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
companion object CREATOR : Parcelable.Creator<Attachment> {
override fun createFromParcel(parcel: Parcel): Attachment {
return Attachment(parcel)
}
override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -25,13 +25,12 @@ import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.*
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageFactory import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
@@ -52,6 +51,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import java.io.* import java.io.*
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class Database { class Database {
@@ -157,6 +157,17 @@ class Database {
} }
} }
fun compressionForNewEntry(): Boolean {
if (mDatabaseKDB != null)
return false
// Default compression not necessary if stored in header
mDatabaseKDBX?.let {
return it.compressionAlgorithm == CompressionAlgorithm.GZip
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
}
return false
}
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm, fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) { newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression) mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
@@ -428,6 +439,37 @@ class Database {
}, omitBackup, max) }, omitBackup, max)
} }
val binaryPool: BinaryPool
get() {
return mDatabaseKDBX?.binaryPool ?: BinaryPool()
}
val allowMultipleAttachments: Boolean
get() {
if (mDatabaseKDB != null)
return false
if (mDatabaseKDBX != null)
return true
return false
}
fun buildNewBinary(cacheDirectory: File,
enableProtection: Boolean = false,
compressed: Boolean = false): BinaryAttachment? {
return mDatabaseKDB?.buildNewBinary(cacheDirectory)
?: mDatabaseKDBX?.buildNewBinary(cacheDirectory, enableProtection, compressed)
}
fun removeAttachmentIfNotUsed(attachment: Attachment) {
// No need in KDB database because unique attachment by entry
mDatabaseKDBX?.removeAttachmentIfNotUsed(attachment)
}
fun removeUnlinkedAttachments() {
// No check in database KDB because unique attachment by entry
mDatabaseKDBX?.removeUnlinkedAttachments()
}
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver) { fun saveData(contentResolver: ContentResolver) {
try { try {
@@ -800,7 +842,7 @@ class Database {
rootGroup?.doForEachChildAndForIt( rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() { object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean { override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node) removeOldestEntryHistory(node, binaryPool)
return true return true
} }
}, },
@@ -808,7 +850,8 @@ class Database {
override fun operate(node: Group): Boolean { override fun operate(node: Group): Boolean {
return true return true
} }
}) }
)
} }
fun removeEachEntryHistory() { fun removeEachEntryHistory() {
@@ -829,9 +872,8 @@ class Database {
/** /**
* Remove oldest history if more than max items or max memory * Remove oldest history if more than max items or max memory
*/ */
fun removeOldestEntryHistory(entry: Entry) { fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
mDatabaseKDBX?.let { mDatabaseKDBX?.let {
val maxItems = historyMaxItems val maxItems = historyMaxItems
if (maxItems >= 0) { if (maxItems >= 0) {
while (entry.getHistory().size > maxItems) { while (entry.getHistory().size > maxItems) {
@@ -844,7 +886,7 @@ class Database {
while (true) { while (true) {
var historySize: Long = 0 var historySize: Long = 0
for (entryHistory in entry.getHistory()) { for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize() historySize += entryHistory.getSize(binaryPool)
} }
if (historySize > maxSize) { if (historySize > maxSize) {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -33,7 +34,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -317,40 +317,38 @@ class Entry : Node, EntryVersionedInterface<Group> {
} }
} }
fun startToManageFieldReferences(db: DatabaseKDBX) { fun startToManageFieldReferences(database: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db) entryKDBX?.startToManageFieldReferences(database)
} }
fun stopToManageFieldReferences() { fun stopToManageFieldReferences() {
entryKDBX?.stopToManageFieldReferences() entryKDBX?.stopToManageFieldReferences()
} }
fun getAttachments(): ArrayList<EntryAttachment> { fun getAttachments(binaryPool: BinaryPool): ArrayList<Attachment> {
val attachments = ArrayList<EntryAttachment>() val attachments = ArrayList<Attachment>()
entryKDB?.getAttachments()?.let {
entryKDB?.binaryData?.let { binaryKDB -> attachments.addAll(it)
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
} }
entryKDBX?.getAttachments(binaryPool)?.let {
entryKDBX?.binaries?.let { binariesKDBX -> attachments.addAll(it)
for ((key, value) in binariesKDBX) {
attachments.add(EntryAttachment(key, value))
}
} }
return attachments return attachments
} }
fun removeAttachment(attachment: EntryAttachment) { fun containsAttachment(): Boolean {
entryKDB?.apply { return entryKDB?.containsAttachment() == true
if (binaryDescription == attachment.name || entryKDBX?.containsAttachment() == true
&& binaryData == attachment.binaryAttachment) { }
binaryDescription = ""
binaryData = null
}
}
entryKDBX?.removeProtectedBinary(attachment.name) fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
entryKDB?.putAttachment(attachment)
entryKDBX?.putAttachment(attachment, binaryPool)
}
fun removeAttachment(attachment: Attachment) {
entryKDB?.removeAttachment(attachment)
entryKDBX?.removeAttachment(attachment)
} }
fun getHistory(): ArrayList<Entry> { fun getHistory(): ArrayList<Entry> {
@@ -380,8 +378,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryKDBX?.removeOldestEntryFromHistory() entryKDBX?.removeOldestEntryFromHistory()
} }
fun getSize(): Long { fun getSize(binaryPool: BinaryPool): Long {
return entryKDBX?.size ?: 0L return entryKDBX?.getSize(binaryPool) ?: 0L
} }
fun containsCustomData(): Boolean { fun containsCustomData(): Boolean {

View File

@@ -17,10 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. * along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
package com.kunzisoft.keepass.database.element.security package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes import com.kunzisoft.keepass.stream.readBytes
@@ -30,7 +28,7 @@ import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable { class BinaryAttachment : Parcelable {
var isCompressed: Boolean? = null var isCompressed: Boolean = false
private set private set
var isProtected: Boolean = false var isProtected: Boolean = false
private set private set
@@ -46,12 +44,12 @@ class BinaryAttachment : Parcelable {
* Empty protected binary * Empty protected binary
*/ */
constructor() { constructor() {
this.isCompressed = null this.isCompressed = false
this.isProtected = false this.isProtected = false
this.dataFile = null this.dataFile = null
} }
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) { constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean = false) {
this.isCompressed = compressed this.isCompressed = compressed
this.isProtected = enableProtection this.isProtected = enableProtection
this.dataFile = dataFile this.dataFile = dataFile
@@ -59,7 +57,7 @@ class BinaryAttachment : Parcelable {
private constructor(parcel: Parcel) { private constructor(parcel: Parcel) {
val compressedByte = parcel.readByte().toInt() val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0 isCompressed = compressedByte != 0
isProtected = parcel.readByte().toInt() != 0 isProtected = parcel.readByte().toInt() != 0
parcel.readString()?.let { parcel.readString()?.let {
dataFile = File(it) dataFile = File(it)
@@ -74,32 +72,51 @@ class BinaryAttachment : Parcelable {
} }
} }
@Throws(IOException::class)
fun getUnGzipInputDataStream(): InputStream {
return if (isCompressed)
GZIPInputStream(getInputDataStream())
else
getInputDataStream()
}
@Throws(IOException::class)
fun getOutputDataStream(): OutputStream {
return when {
dataFile != null -> FileOutputStream(dataFile!!)
else -> throw IOException("Unable to write in an unknown file")
}
}
@Throws(IOException::class)
fun getGzipOutputDataStream(): OutputStream {
return if (isCompressed) {
GZIPOutputStream(getOutputDataStream())
} else {
getOutputDataStream()
}
}
@Throws(IOException::class) @Throws(IOException::class)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file // To compress, create a new binary with file
if (isCompressed != true) { if (!isCompressed) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
var inputStream: InputStream? = null getInputDataStream().use { inputStream ->
try { inputStream.readBytes(bufferSize) { buffer ->
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress)) outputStream.write(buffer)
inputStream = getInputDataStream()
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
} finally {
inputStream?.close()
outputStream?.close()
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
} }
} }
} }
// Remove unGzip file
if (concreteDataFile.delete()) {
if (fileBinaryCompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = true
}
}
} }
} }
} }
@@ -107,52 +124,20 @@ class BinaryAttachment : Parcelable {
@Throws(IOException::class) @Throws(IOException::class)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) { fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile -> dataFile?.let { concreteDataFile ->
if (isCompressed != false) { if (isCompressed) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp") val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null FileOutputStream(fileBinaryDecompress).use { outputStream ->
var inputStream: GZIPInputStream? = null getUnGzipInputDataStream().use { inputStream ->
try { inputStream.readBytes(bufferSize) { buffer ->
outputStream = FileOutputStream(fileBinaryDecompress) outputStream.write(buffer)
inputStream = GZIPInputStream(getInputDataStream())
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
} finally {
inputStream?.close()
outputStream?.close()
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
} }
} }
} }
} // Remove gzip file
} if (concreteDataFile.delete()) {
} if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
fun download(createdFileUri: Uri, isCompressed = false
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0
contentResolver.openOutputStream(createdFileUri).use { outputStream ->
outputStream?.let { fileOutputStream ->
if (isCompressed == true) {
GZIPInputStream(getInputDataStream())
} else {
getInputDataStream()
}.use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
fileOutputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / length()).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {}
} }
} }
} }
@@ -185,18 +170,22 @@ class BinaryAttachment : Parcelable {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = 0 var result = 0
result = 31 * result + if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0 result = 31 * result + if (isCompressed) 1 else 0
result = 31 * result + if (isProtected) 1 else 0 result = 31 * result + if (isProtected) 1 else 0
result = 31 * result + dataFile!!.hashCode() result = 31 * result + dataFile!!.hashCode()
return result return result
} }
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int { override fun describeContents(): Int {
return 0 return 0
} }
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeByte((if (isCompressed == null) 2 else if (isCompressed!!) 1 else 0).toByte()) dest.writeByte((if (isCompressed) 1 else 0).toByte())
dest.writeByte((if (isProtected) 1 else 0).toByte()) dest.writeByte((if (isProtected) 1 else 0).toByte())
dest.writeString(dataFile?.absolutePath) dest.writeString(dataFile?.absolutePath)
} }

View File

@@ -19,52 +19,126 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.SparseArray
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import java.io.IOException import java.io.IOException
class BinaryPool { class BinaryPool {
private val pool = SparseArray<BinaryAttachment>() private val pool = LinkedHashMap<Int, BinaryAttachment>()
/**
* To get a binary by the pool key (ref attribute in entry)
*/
operator fun get(key: Int): BinaryAttachment? { operator fun get(key: Int): BinaryAttachment? {
return pool[key] return pool[key]
} }
fun put(key: Int, value: BinaryAttachment) { /**
pool.put(key, value) * To linked a binary with a pool key, if the pool key doesn't exists, create an unused one
*/
fun put(key: Int?, value: BinaryAttachment) {
if (key == null)
put(value)
else
pool[key] = value
} }
fun doForEachBinary(action: (key: Int, binary: BinaryAttachment) -> Unit) { /**
for (i in 0 until pool.size()) { * To put a [binaryAttachment] in the pool,
action.invoke(i, pool.get(pool.keyAt(i))) * if already exists, replace the current one,
* else add it with a new key
*/
fun put(binaryAttachment: BinaryAttachment): Int {
var key = findKey(binaryAttachment)
if (key == null) {
key = findUnusedKey()
} }
pool[key] = binaryAttachment
return key
} }
/**
* Remove a binary from the pool, the file is not deleted
*/
@Throws(IOException::class) @Throws(IOException::class)
fun clear() { fun remove(binaryAttachment: BinaryAttachment) {
doForEachBinary { _, binary -> findKey(binaryAttachment)?.let {
binary.clear() pool.remove(it)
} }
pool.clear() // Don't clear attachment here because a file can be used in many BinaryAttachment
} }
fun add(fileBinary: BinaryAttachment) { /**
if (findKey(fileBinary) == null) { * Utility method to find an unused key in the pool
pool.put(findUnusedKey(), fileBinary) */
} private fun findUnusedKey(): Int {
} var unusedKey = 0
while (pool[unusedKey] != null)
fun findUnusedKey(): Int {
var unusedKey = pool.size()
while (get(unusedKey) != null)
unusedKey++ unusedKey++
return unusedKey return unusedKey
} }
fun findKey(pb: BinaryAttachment): Int? { /**
for (i in 0 until pool.size()) { * Return key of [binaryAttachmentToRetrieve] or null if not found
if (pool.get(pool.keyAt(i)) == pb) return i */
private fun findKey(binaryAttachmentToRetrieve: BinaryAttachment): Int? {
val contains = pool.containsValue(binaryAttachmentToRetrieve)
return if (!contains)
null
else {
for ((key, binary) in pool) {
if (binary == binaryAttachmentToRetrieve) {
return key
}
}
return null
} }
return null
} }
/**
* Utility method to order binaries and solve index problem in database v4
*/
private fun orderedBinaries(): List<KeyBinary> {
val keyBinaryList = ArrayList<KeyBinary>()
for ((key, binary) in pool) {
keyBinaryList.add(KeyBinary(key, binary))
}
return keyBinaryList
}
/**
* To register a binary with a ref corresponding to an ordered index
*/
fun getBinaryIndexFromKey(key: Int): Int? {
val index = orderedBinaries().indexOfFirst { it.key == key }
return if (index < 0)
null
else
index
}
/**
* Different from doForEach, provide an ordered index to each binary
*/
fun doForEachOrderedBinary(action: (index: Int, keyBinary: KeyBinary) -> Unit) {
orderedBinaries().forEachIndexed(action)
}
/**
* To do an action on each binary in the pool
*/
fun doForEachBinary(action: (binary: BinaryAttachment) -> Unit) {
pool.values.forEach { action.invoke(it) }
}
@Throws(IOException::class)
fun clear() {
doForEachBinary {
it.clear()
}
pool.clear()
}
/**
* Utility data class to order binaries
*/
data class KeyBinary(val key: Int, val binary: BinaryAttachment)
} }

View File

@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.NullOutputStream import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.DigestOutputStream import java.security.DigestOutputStream
@@ -249,6 +250,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
addEntryTo(entry, origParent) addEntryTo(entry, origParent)
} }
fun buildNewBinary(cacheDirectory: File): BinaryAttachment {
// Generate an unique new file with timestamp
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
return BinaryAttachment(fileInCache)
}
companion object { companion object {
val TYPE = DatabaseKDB::class.java val TYPE = DatabaseKDB::class.java

View File

@@ -29,8 +29,10 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -40,12 +42,14 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.UnknownKDF import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node import org.w3c.dom.Node
import org.w3c.dom.Text import org.w3c.dom.Text
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
@@ -173,33 +177,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun changeBinaryCompression(oldCompression: CompressionAlgorithm, fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) { newCompression: CompressionAlgorithm) {
binaryPool.doForEachBinary { key, binary -> when (oldCompression) {
CompressionAlgorithm.None -> {
try { when (newCompression) {
when (oldCompression) { CompressionAlgorithm.None -> {}
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {
}
CompressionAlgorithm.GZip -> {
// To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES)
}
}
}
CompressionAlgorithm.GZip -> { CompressionAlgorithm.GZip -> {
when (newCompression) { // Only in databaseV3.1, in databaseV4 the header is zipped during the save
CompressionAlgorithm.None -> { if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
// To decompress, create a new binary with file compressAllBinaries()
binary.decompress(BUFFER_SIZE_BYTES)
}
CompressionAlgorithm.GZip -> {
}
} }
} }
} }
}
CompressionAlgorithm.GZip -> {
// In databaseV4 the header is zipped during the save, so not necessary here
if (kdbxVersion.toKotlinLong() >= FILE_VERSION_32_4.toKotlinLong()) {
decompressAllBinaries()
} else {
when (newCompression) {
CompressionAlgorithm.None -> {
decompressAllBinaries()
}
CompressionAlgorithm.GZip -> {}
}
}
}
}
}
private fun compressAllBinaries() {
binaryPool.doForEachBinary { binary ->
try {
// To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to change compression for $key") Log.e(TAG, "Unable to compress $binary", e)
}
}
}
private fun decompressAllBinaries() {
binaryPool.doForEachBinary { binary ->
try {
binary.decompress(BUFFER_SIZE_BYTES)
} catch (e: Exception) {
Log.e(TAG, "Unable to decompress $binary", e)
} }
} }
} }
@@ -536,6 +558,52 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return publicCustomData.size() > 0 return publicCustomData.size() > 0
} }
fun buildNewBinary(cacheDirectory: File,
protection: Boolean,
compression: Boolean,
binaryPoolId: Int? = null): BinaryAttachment {
// New file with current time
val fileInCache = File(cacheDirectory, System.currentTimeMillis().toString())
val binaryAttachment = BinaryAttachment(fileInCache, protection, compression)
// add attachment to pool
binaryPool.put(binaryPoolId, binaryAttachment)
return binaryAttachment
}
fun removeAttachmentIfNotUsed(attachment: Attachment) {
// Remove attachment from pool
removeUnlinkedAttachments(attachment.binaryAttachment)
}
fun removeUnlinkedAttachments(vararg binaries: BinaryAttachment) {
// Build binaries to remove with all binaries known
val binariesToRemove = ArrayList<BinaryAttachment>()
if (binaries.isEmpty()) {
binaryPool.doForEachBinary { binary ->
binariesToRemove.add(binary)
}
} else {
binariesToRemove.addAll(binaries)
}
// Remove binaries from the list
rootGroup?.doForEachChild(object : NodeHandler<EntryKDBX>() {
override fun operate(node: EntryKDBX): Boolean {
node.getAttachments(binaryPool).forEach {
binariesToRemove.remove(it.binaryAttachment)
}
return binariesToRemove.isNotEmpty()
}
}, null)
// Effective removing
binariesToRemove.forEach {
try {
binaryPool.remove(it)
} catch (e: Exception) {
Log.w(TAG, "Unable to clean binaries", e)
}
}
}
override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean { override fun validatePasswordEncoding(password: String?, containsKeyFile: Boolean): Boolean {
if (password == null) if (password == null)
return true return true

View File

@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import java.util.* import java.util.*
import kotlin.collections.ArrayList
/** /**
* Structure containing information about one entry. * Structure containing information about one entry.
@@ -135,6 +137,30 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override val type: Type override val type: Type
get() = Type.ENTRY get() = Type.ENTRY
fun getAttachments(): ArrayList<Attachment> {
return ArrayList<Attachment>().apply {
val binary = binaryData
if (binary != null)
add(Attachment(binaryDescription, binary))
}
}
fun containsAttachment(): Boolean {
return binaryData != null
}
fun putAttachment(attachment: Attachment) {
this.binaryDescription = attachment.name
this.binaryData = attachment.binaryAttachment
}
fun removeAttachment(attachment: Attachment) {
if (this.binaryDescription == attachment.name) {
this.binaryDescription = ""
this.binaryData = null
}
}
companion object { companion object {
/** Size of byte buffer needed to hold this struct. */ /** Size of byte buffer needed to hold this struct. */

View File

@@ -31,11 +31,13 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.utils.ParcelableUtil import com.kunzisoft.keepass.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.* import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface { class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@@ -60,8 +62,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
} }
var iconCustom = IconImageCustom.UNKNOWN_ICON var iconCustom = IconImageCustom.UNKNOWN_ICON
private var customData = LinkedHashMap<String, String>() private var customData = LinkedHashMap<String, String>()
// TODO Private
var fields = LinkedHashMap<String, ProtectedString>() var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, BinaryAttachment>() var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = "" var foregroundColor = ""
var backgroundColor = "" var backgroundColor = ""
var overrideURL = "" var overrideURL = ""
@@ -70,36 +73,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var additional = "" var additional = ""
var tags = "" var tags = ""
val size: Long fun getSize(binaryPool: BinaryPool): Long {
get() { var size = FIXED_LENGTH_SIZE
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) { for (entry in fields.entries) {
size += entry.key.length.toLong() size += entry.key.length.toLong()
size += entry.value.length().toLong() size += entry.value.length().toLong()
}
for ((key, value) in binaries) {
size += key.length.toLong()
size += value.length()
}
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.size
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
} }
size += getAttachmentsSize(binaryPool)
size += autoType.defaultSequence.length.toLong()
for ((key, value) in autoType.entrySet()) {
size += key.length.toLong()
size += value.length.toLong()
}
for (entry in history) {
size += entry.getSize(binaryPool)
}
size += overrideURL.length.toLong()
size += tags.length.toLong()
return size
}
override var expires: Boolean = false override var expires: Boolean = false
constructor() : super() constructor() : super()
@@ -110,7 +109,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel) customData = ParcelableUtil.readStringParcelableMap(parcel)
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java) fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java) binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL overrideURL = parcel.readString() ?: overrideURL
@@ -128,7 +127,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(locationChanged, flags) dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData) ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields) ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries) ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor) dest.writeString(foregroundColor)
dest.writeString(backgroundColor) dest.writeString(backgroundColor)
dest.writeString(overrideURL) dest.writeString(overrideURL)
@@ -167,8 +166,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
tags = source.tags tags = source.tags
} }
fun startToManageFieldReferences(db: DatabaseKDBX) { fun startToManageFieldReferences(database: DatabaseKDBX) {
this.mDatabase = db this.mDatabase = database
this.mDecodeRef = true this.mDecodeRef = true
} }
@@ -284,14 +283,38 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fields[label] = value fields[label] = value
} }
fun putProtectedBinary(key: String, value: BinaryAttachment) { fun getAttachments(binaryPool: BinaryPool): ArrayList<Attachment> {
binaries[key] = value val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary))
}
}
return entryAttachmentList
} }
fun removeProtectedBinary(name: String) { fun containsAttachment(): Boolean {
binaries.remove(name) return binaries.isNotEmpty()
} }
fun putAttachment(attachment: Attachment, binaryPool: BinaryPool) {
binaries[attachment.name] = binaryPool.put(attachment.binaryAttachment)
}
fun removeAttachment(attachment: Attachment) {
binaries.remove(attachment.name)
}
private fun getAttachmentsSize(binaryPool: BinaryPool): Long {
var size = 0L
for ((label, poolId) in binaries) {
size += label.length.toLong()
size += binaryPool[poolId]?.length() ?: 0
}
return size
}
// TODO Remove ?
fun sizeOfHistory(): Int { fun sizeOfHistory(): Int {
return history.size return history.size
} }

View File

@@ -27,14 +27,12 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.* import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import org.joda.time.Instant
import java.io.* import java.io.*
import java.security.* import java.security.*
import java.util.* import java.util.*
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> { 0x000E -> {
newEntry?.let { entry -> newEntry?.let { entry ->
if (fieldSize > 0) { if (fieldSize > 0) {
// Generate an unique new file with timestamp val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
val binaryFile = File(cacheDirectory, entry.binaryData = binaryAttachment
Instant.now().millis.toString()) BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
entry.binaryData = BinaryAttachment(binaryFile)
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
cipherInputStream.readBytes(fieldSize, cipherInputStream.readBytes(fieldSize,
DatabaseKDB.BUFFER_SIZE_BYTES) { buffer -> DatabaseKDB.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)

View File

@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -35,7 +36,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.* import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -49,12 +50,14 @@ import org.bouncycastle.crypto.StreamCipher
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
import java.io.* import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset import java.nio.charset.Charset
import java.text.ParseException import java.text.ParseException
import java.util.* import java.util.*
import java.util.zip.GZIPInputStream import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import kotlin.math.min import kotlin.math.min
@@ -68,9 +71,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var hashOfHeader: ByteArray? = null private var hashOfHeader: ByteArray? = null
private val unusedCacheFileName: String
get() = mDatabase.binaryPool.findUnusedKey().toString()
private var readNextNode = true private var readNextNode = true
private val ctxGroups = Stack<GroupKDBX>() private val ctxGroups = Stack<GroupKDBX>()
private var ctxGroup: GroupKDBX? = null private var ctxGroup: GroupKDBX? = null
@@ -233,8 +233,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
var data = ByteArray(0) var data = ByteArray(0)
if (size > 0) { if (size > 0) {
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
// TODO OOM here
data = dataInputStream.readBytes(size) data = dataInputStream.readBytes(size)
}
} }
var result = true var result = true
@@ -249,18 +251,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
header.innerRandomStreamKey = data header.innerRandomStreamKey = data
} }
DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> { DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary -> {
val flag = dataInputStream.readBytes(1)[0].toInt() != 0
val protectedFlag = flag && DatabaseHeaderKDBX.KdbxBinaryFlags.Protected.toInt() != DatabaseHeaderKDBX.KdbxBinaryFlags.None.toInt()
val byteLength = size - 1
// Read in a file // Read in a file
val file = File(cacheDirectory, unusedCacheFileName) val protectedFlag = dataInputStream.readBytes(1)[0].toInt() != 0
FileOutputStream(file).use { outputStream -> val byteLength = size - 1
// No compression at this level
val protectedBinary = mDatabase.buildNewBinary(cacheDirectory, protectedFlag, false)
protectedBinary.getOutputDataStream().use { outputStream ->
dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer -> dataInputStream.readBytes(byteLength, DatabaseKDBX.BUFFER_SIZE_BYTES) { buffer ->
outputStream.write(buffer) outputStream.write(buffer)
} }
} }
val protectedBinary = BinaryAttachment(file, protectedFlag)
mDatabase.binaryPool.add(protectedBinary)
} }
} }
@@ -443,14 +443,7 @@ class DatabaseInputKDBX(cacheDirectory: File,
} }
KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) { KdbContext.Binaries -> if (name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId) readBinary(xpp)
if (key != null) {
val pbData = readBinary(xpp)
val id = Integer.parseInt(key)
mDatabase.binaryPool.put(id, pbData!!)
} else {
readUnknown(xpp)
}
} else { } else {
readUnknown(xpp) readUnknown(xpp)
} }
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
return KdbContext.Entry return KdbContext.Entry
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) { } else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
if (ctxBinaryName != null && ctxBinaryValue != null) if (ctxBinaryName != null && ctxBinaryValue != null) {
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!) ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
}
ctxBinaryName = null ctxBinaryName = null
ctxBinaryValue = null ctxBinaryValue = null
@@ -947,50 +941,56 @@ class DatabaseInputKDBX(cacheDirectory: File,
// Reference Id to a binary already present in binary pool // Reference Id to a binary already present in binary pool
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef) val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
if (ref != null) { // New id to a binary
xpp.next() // Consume end tag val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
val id = Integer.parseInt(ref) return when {
return mDatabase.binaryPool[id] ref != null -> {
} xpp.next() // Consume end tag
val id = Integer.parseInt(ref)
// New binary to retrieve // A ref is not necessarily an index in Database V3.1
else { mDatabase.binaryPool[id]
var compressed = false
var protected = false
if (xpp.attributeCount > 0) {
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
if (compress != null) {
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null) {
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
} }
key != null -> {
val base64 = readString(xpp) createBinary(key.toIntOrNull(), xpp)
if (base64.isEmpty()) }
return BinaryAttachment() else -> {
val data = Base64.decode(base64, BASE_64_FLAG) // New binary to retrieve
createBinary(null, xpp)
val file = File(cacheDirectory, unusedCacheFileName)
return FileOutputStream(file).use { outputStream ->
// Force compression in this specific case
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
&& !compressed) {
GZIPOutputStream(outputStream).write(data)
BinaryAttachment(file, protected, true)
} else {
outputStream.write(data)
BinaryAttachment(file, protected, compressed)
}
} }
} }
} }
@Throws(IOException::class, XmlPullParserException::class)
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
var compressed = false
var protected = false
if (xpp.attributeCount > 0) {
val compress = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrCompressed)
if (compress != null) {
compressed = compress.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
val protect = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrProtected)
if (protect != null) {
protected = protect.equals(DatabaseKDBXXML.ValTrue, ignoreCase = true)
}
}
val base64 = readString(xpp)
if (base64.isEmpty())
return null
val data = Base64.decode(base64, BASE_64_FLAG)
// Build the new binary and compress
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, protected, compressed, binaryId)
binaryAttachment.getOutputDataStream().use { outputStream ->
outputStream.write(data)
}
return binaryAttachment
}
@Throws(IOException::class, XmlPullParserException::class) @Throws(IOException::class, XmlPullParserException::class)
private fun readString(xpp: XmlPullParser): String { private fun readString(xpp: XmlPullParser): String {
val buf = readProtectedBase64String(xpp) val buf = readProtectedBase64String(xpp)

View File

@@ -47,18 +47,25 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
dataOutputStream.writeInt(streamKeySize) dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey) dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachBinary { _, protectedBinary -> database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) { if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
} }
// Force decompression to add binary in header
protectedBinary.decompress()
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt()) dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify dataOutputStream.writeInt(protectedBinary.length().toInt() + 1)
dataOutputStream.write(flag.toInt()) dataOutputStream.write(flag.toInt())
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer -> // if was compressed in cache, uncompress it
dataOutputStream.write(buffer) protectedBinary.getInputDataStream().use { inputStream ->
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
}
} }
} }

View File

@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -55,7 +55,6 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeBinary(binary : BinaryAttachment) { private fun writeBinary(binary : BinaryAttachment) {
val binaryLength = binary.length() val binaryLength = binary.length()
if (binaryLength > 0) { if (binaryLength > 0) {
if (binary.isProtected) { if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(charArray, 0, charArray.size) xml.text(charArray, 0, charArray.size)
} }
} else { } else {
// Force binary compression from database (compression was harmonized during import) if (binary.isCompressed) {
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue) xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
} }
// Force decompression in this specific case
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
&& binary.isCompressed == true) {
GZIPInputStream(binary.getInputDataStream())
} else {
binary.getInputDataStream()
}
// Write the XML // Write the XML
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer -> binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray() val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size) xml.text(charArray, 0, charArray.size)
} }
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMetaBinaries() { private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries) xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary -> // Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary) xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString()) xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(binary) writeBinary(keyBinary.binary)
xml.endTag(null, DatabaseKDBXXML.ElemBinary) xml.endTag(null, DatabaseKDBXXML.ElemBinary)
} }
@@ -559,23 +548,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) { private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
for ((key, binary) in binaries) { for ((label, poolId) in binaries) {
xml.startTag(null, DatabaseKDBXXML.ElemBinary) // Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
xml.startTag(null, DatabaseKDBXXML.ElemKey) mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
xml.text(safeXmlString(key)) xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.endTag(null, DatabaseKDBXXML.ElemKey) xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(label))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
xml.startTag(null, DatabaseKDBXXML.ElemValue) xml.startTag(null, DatabaseKDBXXML.ElemValue)
val ref = mDatabaseKDBX.binaryPool.findKey(binary) // Use only pool data in Meta to save binaries
if (ref != null) { xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString()) xml.endTag(null, DatabaseKDBXXML.ElemValue)
} else {
writeBinary(binary) xml.endTag(null, DatabaseKDBXXML.ElemBinary)
} }
xml.endTag(null, DatabaseKDBXXML.ElemValue)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
} }
} }

View File

@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.security.BinaryAttachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.utils.readEnum import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum import com.kunzisoft.keepass.utils.writeEnum
data class EntryAttachment(var name: String, data class EntryAttachmentState(var attachment: Attachment,
var binaryAttachment: BinaryAttachment, var streamDirection: StreamDirection,
var downloadState: AttachmentState = AttachmentState.NULL, var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable { var downloadProgression: Int = 0) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString() ?: "", parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(), parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL, parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt()) parcel.readInt())
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name) parcel.writeParcelable(attachment, flags)
parcel.writeParcelable(binaryAttachment, flags) parcel.writeEnum(streamDirection)
parcel.writeEnum(downloadState) parcel.writeEnum(downloadState)
parcel.writeInt(downloadProgression) parcel.writeInt(downloadProgression)
} }
@@ -49,26 +50,23 @@ data class EntryAttachment(var name: String,
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is EntryAttachment) return false if (other !is EntryAttachmentState) return false
if (name != other.name) return false if (attachment != other.attachment) return false
if (binaryAttachment != other.binaryAttachment) return false
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = name.hashCode() return attachment.hashCode()
result = 31 * result + binaryAttachment.hashCode()
return result
} }
companion object CREATOR : Parcelable.Creator<EntryAttachment> { companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
override fun createFromParcel(parcel: Parcel): EntryAttachment { override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
return EntryAttachment(parcel) return EntryAttachmentState(parcel)
} }
override fun newArray(size: Int): Array<EntryAttachment?> { override fun newArray(size: Int): Array<EntryAttachmentState?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
} }

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.model
enum class StreamDirection {
UPLOAD, DOWNLOAD
}

View File

@@ -28,17 +28,24 @@ import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.stream.readBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.BufferedInputStream
import java.util.* import java.util.*
import kotlin.collections.HashMap import java.util.concurrent.CopyOnWriteArrayList
class AttachmentFileNotificationService: LockNotificationService() { class AttachmentFileNotificationService: LockNotificationService() {
override val notificationId: Int = 10000 override val notificationId: Int = 10000
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
private var mActionTaskBinder = ActionTaskBinder() private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>() private var mActionTaskListeners = LinkedList<ActionTaskListener>()
@@ -51,33 +58,31 @@ class AttachmentFileNotificationService: LockNotificationService() {
fun addActionTaskListener(actionTaskListener: ActionTaskListener) { fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.add(actionTaskListener) mActionTaskListeners.add(actionTaskListener)
attachmentNotificationList.forEach {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit { it.attachmentFileAction?.listener = attachmentFileActionListener
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) { }
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(entry.key, attachment)
}
}
}
})
} }
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) { fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit { attachmentNotificationList.forEach {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) { it.attachmentFileAction?.listener = null
entry.value.attachmentTask?.onUpdate = null }
}
})
mActionTaskListeners.remove(actionTaskListener) mActionTaskListeners.remove(actionTaskListener)
} }
} }
private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
override fun onUpdate(attachmentNotification: AttachmentNotification) {
newNotification(attachmentNotification)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentAction(attachmentNotification.uri,
attachmentNotification.entryAttachmentState)
}
}
}
interface ActionTaskListener { interface ActionTaskListener {
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
} }
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
@@ -87,49 +92,29 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) { val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY) intent.getParcelableExtra(FILE_URI_KEY)
} else null } else null
when(intent?.action) { when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.UPLOAD)
}
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> { ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
if (downloadFileUri != null actionUploadOrDownload(downloadFileUri,
&& intent.hasExtra(ATTACHMENT_KEY)) { intent,
StreamDirection.DOWNLOAD)
val nextNotificationId = (downloadFileUris.values.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
try {
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
mainScope.launch {
AttachmentFileActionClass(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e)
}
}
} }
else -> { else -> {
if (downloadFileUri != null) { if (downloadFileUri != null) {
downloadFileUris[downloadFileUri]?.notificationId?.let { attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
notificationManager?.cancel(it) notificationManager?.cancel(elementToRemove.notificationId)
downloadFileUris.remove(downloadFileUri) attachmentNotificationList.remove(elementToRemove)
} }
} }
if (downloadFileUris.isEmpty()) { if (attachmentNotificationList.isEmpty()) {
stopSelf() stopSelf()
} }
} }
@@ -138,25 +123,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
@Synchronized
fun checkCurrentAttachmentProgress() { fun checkCurrentAttachmentProgress() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit { attachmentNotificationList.forEach { attachmentNotification ->
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) { mActionTaskListeners.forEach { actionListener ->
mActionTaskListeners.forEach { actionListener -> actionListener.onAttachmentAction(
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment) attachmentNotification.uri,
} attachmentNotification.entryAttachmentState
)
} }
}) }
} }
private fun newNotification(downloadFileUri: Uri, @Synchronized
entryAttachment: EntryAttachment, fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
notificationIdAttachment: Int) { attachmentNotificationList.firstOrNull {
it.entryAttachmentState == entryAttachment
}?.let {
attachmentNotificationList.remove(it)
}
}
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this, val pendingContentIntent = PendingIntent.getActivity(this,
0, 0,
Intent().apply { Intent().apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri)) setDataAndType(attachmentNotification.uri,
contentResolver.getType(attachmentNotification.uri))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, PendingIntent.FLAG_CANCEL_CURRENT)
@@ -164,54 +159,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
0, 0,
Intent(this, AttachmentFileNotificationService::class.java).apply { Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service // No action to delete the service
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri) putExtra(FILE_URI_KEY, attachmentNotification.uri)
}, PendingIntent.FLAG_CANCEL_CURRENT) }, PendingIntent.FLAG_CANCEL_CURRENT)
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: "" val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
val builder = buildNewNotification().apply { val builder = buildNewNotification().apply {
setSmallIcon(R.drawable.ic_file_download_white_24dp) when (attachmentNotification.entryAttachmentState.streamDirection) {
setContentTitle(getString(R.string.download_attachment, fileName)) StreamDirection.UPLOAD -> {
setSmallIcon(R.drawable.ic_file_upload_white_24dp)
setContentTitle(getString(R.string.upload_attachment, fileName))
}
StreamDirection.DOWNLOAD -> {
setSmallIcon(R.drawable.ic_file_download_white_24dp)
setContentTitle(getString(R.string.download_attachment, fileName))
}
}
setAutoCancel(false) setAutoCancel(false)
when (entryAttachment.downloadState) { when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.START -> { AttachmentState.NULL, AttachmentState.START -> {
setContentText(getString(R.string.download_initialization)) setContentText(getString(R.string.download_initialization))
setOngoing(true) setOngoing(true)
} }
AttachmentState.IN_PROGRESS -> { AttachmentState.IN_PROGRESS -> {
if (entryAttachment.downloadProgression > 100) { if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
setContentText(getString(R.string.download_finalization)) setContentText(getString(R.string.download_finalization))
} else { } else {
setProgress(100, entryAttachment.downloadProgression, false) setProgress(100,
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression)) attachmentNotification.entryAttachmentState.downloadProgression,
false)
setContentText(getString(R.string.download_progression,
attachmentNotification.entryAttachmentState.downloadProgression))
} }
setOngoing(true) setOngoing(true)
} }
AttachmentState.COMPLETE, AttachmentState.ERROR -> { AttachmentState.COMPLETE -> {
setContentText(getString(R.string.download_complete)) setContentText(getString(R.string.download_complete))
setContentIntent(pendingContentIntent) when (attachmentNotification.entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
}
StreamDirection.DOWNLOAD -> {
setContentIntent(pendingContentIntent)
}
}
setDeleteIntent(pendingDeleteIntent) setDeleteIntent(pendingDeleteIntent)
setOngoing(false) setOngoing(false)
} }
AttachmentState.ERROR -> {
setContentText(getString(R.string.error_file_not_create))
setOngoing(false)
}
}
}
when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.ERROR,
AttachmentState.COMPLETE -> {
stopForeground(false)
notificationManager?.notify(attachmentNotification.notificationId, builder.build())
} else -> {
startForeground(attachmentNotification.notificationId, builder.build())
} }
} }
notificationManager?.notify(notificationIdAttachment, builder.build())
} }
override fun onDestroy() { override fun onDestroy() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit { attachmentNotificationList.forEach { attachmentNotification ->
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) { attachmentNotification.attachmentFileAction?.listener = null
entry.value.attachmentTask?.onUpdate = null notificationManager?.cancel(attachmentNotification.notificationId)
notificationManager?.cancel(entry.value.notificationId) }
} attachmentNotificationList.clear()
})
super.onDestroy() super.onDestroy()
} }
private data class AttachmentNotification(var notificationId: Int, private data class AttachmentNotification(var uri: Uri,
var entryAttachment: EntryAttachment, var notificationId: Int,
var attachmentTask: AttachmentFileActionClass? = null) { var entryAttachmentState: EntryAttachmentState,
var attachmentFileAction: AttachmentFileAction? = null) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@@ -228,52 +253,85 @@ class AttachmentFileNotificationService: LockNotificationService() {
} }
} }
private class AttachmentFileActionClass( private fun actionUploadOrDownload(downloadFileUri: Uri?,
private val fileUri: Uri, intent: Intent,
streamDirection: StreamDirection) {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
try {
intent.getParcelableExtra<Attachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val nextNotificationId = (attachmentNotificationList.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
val entryAttachmentState = EntryAttachmentState(entryAttachment, streamDirection)
val attachmentNotification = AttachmentNotification(downloadFileUri, nextNotificationId, entryAttachmentState)
attachmentNotificationList.add(attachmentNotification)
mainScope.launch {
AttachmentFileAction(attachmentNotification,
contentResolver).apply {
listener = attachmentFileActionListener
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to upload/download $downloadFileUri", e)
}
}
}
private class AttachmentFileAction(
private val attachmentNotification: AttachmentNotification, private val attachmentNotification: AttachmentNotification,
private val contentResolver: ContentResolver) { private val contentResolver: ContentResolver) {
private val updateMinFrequency = 1000 private val updateMinFrequency = 1000
private var previousSaveTime = System.currentTimeMillis() private var previousSaveTime = System.currentTimeMillis()
var onUpdate: ((Uri, EntryAttachment, Int)->Unit)? = null var listener: AttachmentFileActionListener? = null
interface AttachmentFileActionListener {
fun onUpdate(attachmentNotification: AttachmentNotification)
}
suspend fun executeAction() { suspend fun executeAction() {
TimeoutHelper.temporarilyDisableTimeout() TimeoutHelper.temporarilyDisableTimeout()
// on pre execute // on pre execute
attachmentNotification.attachmentTask = this attachmentNotification.attachmentFileAction = this
attachmentNotification.entryAttachment.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START downloadState = AttachmentState.START
downloadProgression = 0 downloadProgression = 0
} }
onUpdate?.invoke(fileUri, listener?.onUpdate(attachmentNotification)
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// on Progress with thread // on Progress with thread
val asyncResult: Deferred<Boolean> = async { val asyncResult: Deferred<Boolean> = async {
var progressResult = true var progressResult = true
try { try {
attachmentNotification.entryAttachment.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
// Publish progress when (streamDirection) {
val currentTime = System.currentTimeMillis() StreamDirection.UPLOAD -> {
if (previousSaveTime + updateMinFrequency < currentTime) { uploadToDatabase(
attachmentNotification.entryAttachment.apply { attachmentNotification.uri,
downloadState = AttachmentState.IN_PROGRESS attachment.binaryAttachment,
downloadProgression = percent contentResolver, 1024) { percent ->
publishProgress(percent)
}
}
StreamDirection.DOWNLOAD -> {
downloadFromDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
contentResolver, 1024) { percent ->
publishProgress(percent)
} }
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
Log.d(TAG, "Download file $fileUri : $percent%")
previousSaveTime = currentTime
} }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to upload or download file", e)
progressResult = false progressResult = false
} }
progressResult progressResult
@@ -282,33 +340,95 @@ class AttachmentFileNotificationService: LockNotificationService() {
// on post execute // on post execute
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val result = asyncResult.await() val result = asyncResult.await()
attachmentNotification.attachmentTask = null attachmentNotification.attachmentFileAction = null
attachmentNotification.entryAttachment.apply { attachmentNotification.entryAttachmentState.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100 downloadProgression = 100
} }
onUpdate?.invoke(fileUri, listener?.onUpdate(attachmentNotification)
attachmentNotification.entryAttachment, TimeoutHelper.releaseTemporarilyDisableTimeout()
attachmentNotification.notificationId)
} }
} }
} }
fun downloadFromDatabase(attachmentToUploadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataDownloaded = 0L
val fileSize = binaryAttachment.length()
UriUtil.getUriOutputStream(contentResolver, attachmentToUploadUri)?.use { outputStream ->
binaryAttachment.getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
dataDownloaded += buffer.size
try {
val percentDownload = (100 * dataDownloaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
fun uploadToDatabase(attachmentFromDownloadUri: Uri,
binaryAttachment: BinaryAttachment,
contentResolver: ContentResolver,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
update: ((percent: Int)->Unit)? = null) {
var dataUploaded = 0L
val fileSize = contentResolver.openFileDescriptor(attachmentFromDownloadUri, "r")?.statSize ?: 0
UriUtil.getUriInputStream(contentResolver, attachmentFromDownloadUri)?.let { inputStream ->
binaryAttachment.getGzipOutputDataStream().use { outputStream ->
BufferedInputStream(inputStream).use { attachmentBufferedInputStream ->
attachmentBufferedInputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
dataUploaded += buffer.size
try {
val percentDownload = (100 * dataUploaded / fileSize).toInt()
update?.invoke(percentDownload)
} catch (e: Exception) {
Log.e(TAG, "", e)
}
}
}
}
}
}
private fun publishProgress(percent: Int) {
// Publish progress
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
}
CoroutineScope(Dispatchers.Main).launch {
listener?.onUpdate(attachmentNotification)
Log.d(TAG, "Download file ${attachmentNotification.uri} : $percent%")
}
previousSaveTime = currentTime
}
}
companion object { companion object {
private val TAG = AttachmentFileActionClass::class.java.name private val TAG = AttachmentFileAction::class.java.name
} }
} }
companion object { companion object {
private val TAG = AttachmentFileNotificationService::javaClass.name private val TAG = AttachmentFileNotificationService::javaClass.name
const val ACTION_ATTACHMENT_FILE_START_UPLOAD = "ACTION_ATTACHMENT_FILE_START_UPLOAD"
const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD" const val ACTION_ATTACHMENT_FILE_START_DOWNLOAD = "ACTION_ATTACHMENT_FILE_START_DOWNLOAD"
const val DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY" const val FILE_URI_KEY = "FILE_URI_KEY"
const val ATTACHMENT_KEY = "ATTACHMENT_KEY" const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
} }
} }

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2017 Brian Pellin, Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.stream
import java.io.IOException
import java.io.OutputStream
import java.io.RandomAccessFile
class RandomFileOutputStream internal constructor(private val mFile: RandomAccessFile) : OutputStream() {
@Throws(IOException::class)
override fun close() {
super.close()
mFile.close()
}
@Throws(IOException::class)
override fun write(buffer: ByteArray, offset: Int, count: Int) {
super.write(buffer, offset, count)
mFile.write(buffer, offset, count)
}
@Throws(IOException::class)
override fun write(buffer: ByteArray) {
super.write(buffer)
mFile.write(buffer)
}
@Throws(IOException::class)
override fun write(oneByte: Int) {
mFile.write(oneByte)
}
@Throws(IOException::class)
fun seek(pos: Long) {
mFile.seek(pos)
}
}

View File

@@ -28,9 +28,12 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
class AttachmentFileBinderManager(private val activity: FragmentActivity) { class AttachmentFileBinderManager(private val activity: FragmentActivity) {
@@ -43,8 +46,18 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
private var mServiceConnection: ServiceConnection? = null private var mServiceConnection: ServiceConnection? = null
private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener { private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) { override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
onActionTaskListener?.onAttachmentProgress(fileUri, attachment) onActionTaskListener?.let {
it.onAttachmentAction(fileUri, entryAttachmentState)
when (entryAttachmentState.downloadState) {
AttachmentState.COMPLETE,
AttachmentState.ERROR -> {
// Finish the action when capture by activity
consummeAttachmentAction(entryAttachmentState)
}
else -> {}
}
}
} }
} }
@@ -85,22 +98,32 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
mServiceConnection = null mServiceConnection = null
} }
@Synchronized
fun consummeAttachmentAction(attachment: EntryAttachmentState) {
mBinder?.getService()?.removeAttachmentAction(attachment)
}
@Synchronized @Synchronized
private fun start(bundle: Bundle? = null, actionTask: String) { private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(mIntentTask)
if (bundle != null) if (bundle != null)
mIntentTask.putExtras(bundle) mIntentTask.putExtras(bundle)
activity.runOnUiThread { mIntentTask.action = actionTask
mIntentTask.action = actionTask activity.startService(mIntentTask)
activity.startService(mIntentTask) }
}
fun startUploadAttachment(uploadFileUri: Uri,
attachment: Attachment) {
start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, uploadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, ACTION_ATTACHMENT_FILE_START_UPLOAD)
} }
fun startDownloadAttachment(downloadFileUri: Uri, fun startDownloadAttachment(downloadFileUri: Uri,
entryAttachment: EntryAttachment) { attachment: Attachment) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.DOWNLOAD_FILE_URI_KEY, downloadFileUri) putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, downloadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, entryAttachment) putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, ACTION_ATTACHMENT_FILE_START_DOWNLOAD) }, ACTION_ATTACHMENT_FILE_START_DOWNLOAD)
} }
} }

View File

@@ -60,6 +60,15 @@ object ParcelableUtil {
} }
} }
// For writing map with string key and Int value to a Parcel
fun writeStringIntMap(parcel: Parcel, map: LinkedHashMap<String, Int>) {
parcel.writeInt(map.size)
for ((key, value) in map) {
parcel.writeString(key)
parcel.writeInt(value)
}
}
// For reading map with string key from a Parcel // For reading map with string key from a Parcel
fun <V : Parcelable> readStringParcelableMap( fun <V : Parcelable> readStringParcelableMap(
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> { parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
@@ -74,6 +83,19 @@ object ParcelableUtil {
return map return map
} }
// For reading map with string key and Int value from a Parcel
fun readStringIntMap(parcel: Parcel): LinkedHashMap<String, Int> {
val size = parcel.readInt()
val map = LinkedHashMap<String, Int>(size)
for (i in 0 until size) {
val key: String? = parcel.readString()
val value: Int? = parcel.readInt()
if (key != null && value != null)
map[key] = value
}
return map
}
// For writing map with string key and string value to a Parcel // For writing map with string key and string value to a Parcel
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) { fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {

View File

@@ -27,10 +27,7 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import java.io.File import java.io.*
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.util.* import java.util.*
@@ -52,6 +49,17 @@ object UriUtil {
} }
} }
@Throws(FileNotFoundException::class)
fun getUriOutputStream(contentResolver: ContentResolver, fileUri: Uri?): OutputStream? {
if (fileUri == null)
return null
return when {
isFileScheme(fileUri) -> fileUri.path?.let { FileOutputStream(it) }
isContentScheme(fileUri) -> contentResolver.openOutputStream(fileUri)
else -> null
}
}
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? { fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
if (fileUri == null) if (fileUri == null)

View File

@@ -35,9 +35,11 @@ import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.search.UuidUtil import com.kunzisoft.keepass.database.search.UuidUtil
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.otp.OtpType
import java.util.* import java.util.*
@@ -69,7 +71,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val attachmentsContainerView: View private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView private val attachmentsListView: RecyclerView
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, false) private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private val historyContainerView: View private val historyContainerView: View
private val historyListView: RecyclerView private val historyListView: RecyclerView
@@ -105,7 +107,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView = findViewById(R.id.entry_attachments_container) attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list) attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsListView?.apply { attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
@@ -315,17 +317,18 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
} }
fun assignAttachments(attachments: ArrayList<EntryAttachment>, fun assignAttachments(attachments: ArrayList<Attachment>,
onAttachmentClicked: (attachment: EntryAttachment)->Unit) { streamDirection: StreamDirection,
onAttachmentClicked: (attachment: Attachment)->Unit) {
showAttachments(attachments.isNotEmpty()) showAttachments(attachments.isNotEmpty())
attachmentsAdapter.assignItems(attachments) attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onItemClickListener = { item -> attachmentsAdapter.onItemClickListener = { item ->
onAttachmentClicked.invoke(item) onAttachmentClicked.invoke(item.attachment)
} }
} }
fun updateAttachmentDownloadProgress(attachmentToDownload: EntryAttachment) { fun putAttachment(attachmentToDownload: EntryAttachmentState) {
attachmentsAdapter.updateProgress(attachmentToDownload) attachmentsAdapter.putItem(attachmentToDownload)
} }
/* ------------- /* -------------

View File

@@ -38,9 +38,11 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.EntryAttachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.Field import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.model.StreamDirection
import org.joda.time.Duration import org.joda.time.Duration
import org.joda.time.Instant import org.joda.time.Instant
@@ -68,7 +70,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
private val attachmentsListView: RecyclerView private val attachmentsListView: RecyclerView
private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context) private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, true) private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private var iconColor: Int = 0 private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate()) private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
@@ -124,7 +126,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
} }
} }
attachmentsListView?.apply { attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
@@ -242,7 +244,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* ------------- * -------------
*/ */
fun getExtraField(): MutableList<Field> { fun getExtraFields(): List<Field> {
return extraFieldsAdapter.itemsList return extraFieldsAdapter.itemsList
} }
@@ -278,15 +280,41 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
* ------------- * -------------
*/ */
fun assignAttachments(attachments: ArrayList<EntryAttachment>, fun getAttachments(): List<Attachment> {
onDeleteItem: (attachment: EntryAttachment)->Unit) { return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: ArrayList<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments) attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onDeleteButtonClickListener = { item -> attachmentsAdapter.onDeleteButtonClickListener = { item ->
onDeleteItem.invoke(item) onDeleteItem.invoke(item.attachment)
} }
} }
fun containsAttachment(): Boolean {
return !attachmentsAdapter.isEmpty()
}
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
return attachmentsAdapter.contains(attachment)
}
fun putAttachment(attachment: EntryAttachmentState) {
attachmentsContainerView.visibility = View.VISIBLE
attachmentsAdapter.putItem(attachment)
}
fun removeAttachment(attachment: EntryAttachmentState) {
attachmentsAdapter.removeItem(attachment)
}
fun clearAttachments() {
attachmentsAdapter.clear()
}
/** /**
* Validate or not the entry form * Validate or not the entry form
* *

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="true"
android:drawable="@drawable/ic_file_upload_white_24dp" />
<item android:state_activated="false"
android:drawable="@drawable/ic_file_download_white_24dp" />
</selector>

View File

@@ -0,0 +1,5 @@
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_file_download_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

View File

@@ -31,7 +31,6 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container" app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
@@ -95,7 +94,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_file_download_white_24dp" android:src="@drawable/ic_file_stream_white_24dp"
android:contentDescription="@string/download" android:contentDescription="@string/download"
style="@style/KeepassDXStyle.ImageButton.Simple" /> style="@style/KeepassDXStyle.ImageButton.Simple" />
<ProgressBar <ProgressBar

View File

@@ -25,7 +25,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Selectable.Item"> style="@style/KeepassDXStyle.Selectable.Item">
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
@@ -46,20 +46,26 @@
android:layout_marginEnd="@dimen/image_list_margin" android:layout_marginEnd="@dimen/image_list_margin"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@drawable/ic_blank_32dp" android:src="@drawable/ic_blank_32dp"
android:layout_centerVertical="true" app:layout_constraintTop_toTopOf="parent"
android:layout_alignParentLeft="true" app:layout_constraintBottom_toBottomOf="parent"
android:layout_alignParentStart="true" /> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="2dp" android:paddingTop="2dp"
android:paddingBottom="4dp" android:paddingBottom="4dp"
android:layout_centerVertical="true" android:layout_marginLeft="@dimen/image_list_margin"
android:layout_alignParentRight="true" android:layout_marginStart="@dimen/image_list_margin"
android:layout_alignParentEnd="true" android:layout_marginRight="@dimen/image_list_margin"
android:layout_toRightOf="@+id/node_icon" android:layout_marginEnd="@dimen/image_list_margin"
android:layout_toEndOf="@+id/node_icon"> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/node_icon"
app:layout_constraintLeft_toRightOf="@+id/node_icon"
app:layout_constraintEnd_toStartOf="@+id/node_attachment_icon"
app:layout_constraintRight_toLeftOf="@+id/node_attachment_icon">
<TextView <TextView
android:id="@+id/node_text" android:id="@+id/node_text"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -78,5 +84,18 @@
android:singleLine="true" android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle" /> style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle" />
</LinearLayout> </LinearLayout>
</RelativeLayout> <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_attachment_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/image_list_margin"
android:layout_marginStart="@dimen/image_list_margin"
android:layout_marginRight="@dimen/image_list_margin"
android:layout_marginEnd="@dimen/image_list_margin"
android:src="@drawable/ic_attach_file_white_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -26,13 +26,11 @@
android:title="@string/entry_add_field" android:title="@string/entry_add_field"
android:orderInCategory="92" android:orderInCategory="92"
app:showAsAction="always" /> app:showAsAction="always" />
<!--
<item android:id="@+id/menu_add_attachment" <item android:id="@+id/menu_add_attachment"
android:icon="@drawable/ic_attach_file_white_24dp" android:icon="@drawable/ic_attach_file_white_24dp"
android:title="@string/entry_add_attachment" android:title="@string/entry_add_attachment"
android:orderInCategory="93" android:orderInCategory="93"
app:showAsAction="always" /> app:showAsAction="always" />
-->
<item android:id="@+id/menu_add_otp" <item android:id="@+id/menu_add_otp"
android:icon="@drawable/ic_otp_white_24dp" android:icon="@drawable/ic_otp_white_24dp"
android:title="@string/entry_setup_otp" android:title="@string/entry_setup_otp"

View File

@@ -43,7 +43,7 @@
<string name="entry_title">العنوان</string> <string name="entry_title">العنوان</string>
<string name="entry_url">رابط</string> <string name="entry_url">رابط</string>
<string name="entry_user_name">اسم المستخدم</string> <string name="entry_user_name">اسم المستخدم</string>
<string name="error_file_not_create">تعذر إنشاء الملف :</string> <string name="error_file_not_create">تعذر إنشاء الملف</string>
<string name="error_invalid_path">تأكد أن المسار صحيح.</string> <string name="error_invalid_path">تأكد أن المسار صحيح.</string>
<string name="error_no_name">ادخل اسمًا.</string> <string name="error_no_name">ادخل اسمًا.</string>
<string name="error_pass_match">كلمتا السر غير متطابقتين.</string> <string name="error_pass_match">كلمتا السر غير متطابقتين.</string>

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">Usuari</string> <string name="entry_user_name">Usuari</string>
<string name="error_arc4">L\'encriptació Arcfour no està suportada.</string> <string name="error_arc4">L\'encriptació Arcfour no està suportada.</string>
<string name="error_can_not_handle_uri">KeePassDX no pot manejar aquesta URI.</string> <string name="error_can_not_handle_uri">KeePassDX no pot manejar aquesta URI.</string>
<string name="error_file_not_create">No s\'ha pogut crear l\'arxiu:</string> <string name="error_file_not_create">No s\'ha pogut crear l\'arxiu</string>
<string name="error_invalid_db">No s\'ha pogut llegir la base de dades.</string> <string name="error_invalid_db">No s\'ha pogut llegir la base de dades.</string>
<string name="error_invalid_path">Assegureu-vos que el camí eś correcte.</string> <string name="error_invalid_path">Assegureu-vos que el camí eś correcte.</string>
<string name="error_no_name">Introduïu-hi un nom.</string> <string name="error_no_name">Introduïu-hi un nom.</string>

View File

@@ -59,7 +59,7 @@
<string name="entry_user_name">Uživatelské jméno</string> <string name="entry_user_name">Uživatelské jméno</string>
<string name="error_arc4">Arcfour proudová šifra není podporována.</string> <string name="error_arc4">Arcfour proudová šifra není podporována.</string>
<string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string> <string name="error_can_not_handle_uri">KeePassDX nemůže zpracovat toto URI.</string>
<string name="error_file_not_create">Soubor se nedaří vytvořit:</string> <string name="error_file_not_create">Soubor se nedaří vytvořit</string>
<string name="error_invalid_db">Nelze přečíst databázi.</string> <string name="error_invalid_db">Nelze přečíst databázi.</string>
<string name="error_invalid_path">Neplatná cesta.</string> <string name="error_invalid_path">Neplatná cesta.</string>
<string name="error_no_name">Vložte jméno.</string> <string name="error_no_name">Vložte jméno.</string>
@@ -419,7 +419,7 @@
<string name="database_custom_color_title">Vlastní barva databáze</string> <string name="database_custom_color_title">Vlastní barva databáze</string>
<string name="compression">Komprese</string> <string name="compression">Komprese</string>
<string name="compression_none">Žádná</string> <string name="compression_none">Žádná</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Nastavení klávesnice zařízení</string> <string name="device_keyboard_setting_title">Nastavení klávesnice zařízení</string>
<string name="error_save_database">Nebylo možno uložit databázi.</string> <string name="error_save_database">Nebylo možno uložit databázi.</string>
<string name="menu_save_database">Uložit databázi</string> <string name="menu_save_database">Uložit databázi</string>
@@ -440,7 +440,7 @@
<string name="download_initialization">Zahajuji…</string> <string name="download_initialization">Zahajuji…</string>
<string name="download_progression">Probíhá: %1$d%%</string> <string name="download_progression">Probíhá: %1$d%%</string>
<string name="download_finalization">Dokončuji…</string> <string name="download_finalization">Dokončuji…</string>
<string name="download_complete">Ukončeno! Klepnout pro otevření souboru.</string> <string name="download_complete">Klepnout pro otevření souboru</string>
<string name="hide_expired_entries_title">Skrýt propadlé záznamy</string> <string name="hide_expired_entries_title">Skrýt propadlé záznamy</string>
<string name="hide_expired_entries_summary">Propadlé záznamy jsou skryty</string> <string name="hide_expired_entries_summary">Propadlé záznamy jsou skryty</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Brugernavn</string> <string name="entry_user_name">Brugernavn</string>
<string name="error_arc4">Arcfour stream cipher er ikke understøttet.</string> <string name="error_arc4">Arcfour stream cipher er ikke understøttet.</string>
<string name="error_can_not_handle_uri">Kunne ikke håndtere URI i KeePassDX.</string> <string name="error_can_not_handle_uri">Kunne ikke håndtere URI i KeePassDX.</string>
<string name="error_file_not_create">Kunne ikke oprette fil:</string> <string name="error_file_not_create">Kunne ikke oprette fil</string>
<string name="error_invalid_db">Kunne ikke læse databasen.</string> <string name="error_invalid_db">Kunne ikke læse databasen.</string>
<string name="error_invalid_path">Sørg for, at stien er korrekt.</string> <string name="error_invalid_path">Sørg for, at stien er korrekt.</string>
<string name="error_no_name">Indtast et navn.</string> <string name="error_no_name">Indtast et navn.</string>
@@ -419,7 +419,7 @@
<string name="database_custom_color_title">Brugerdefineret databasefarve</string> <string name="database_custom_color_title">Brugerdefineret databasefarve</string>
<string name="compression">Komprimering</string> <string name="compression">Komprimering</string>
<string name="compression_none">Ingen</string> <string name="compression_none">Ingen</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Indstillinger for enhedens tastatur</string> <string name="device_keyboard_setting_title">Indstillinger for enhedens tastatur</string>
<string name="error_save_database">Databasen kunne ikke gemmes.</string> <string name="error_save_database">Databasen kunne ikke gemmes.</string>
<string name="menu_save_database">Gem database</string> <string name="menu_save_database">Gem database</string>
@@ -440,7 +440,7 @@
<string name="download_initialization">Initialiserer…</string> <string name="download_initialization">Initialiserer…</string>
<string name="download_progression">I gang: %1$d%%</string> <string name="download_progression">I gang: %1$d%%</string>
<string name="download_finalization">Færdiggørelse…</string> <string name="download_finalization">Færdiggørelse…</string>
<string name="download_complete">Komplet! Tryk for at åbne filen.</string> <string name="download_complete">Komplet!</string>
<string name="hide_expired_entries_title">Skjul udløbne poster</string> <string name="hide_expired_entries_title">Skjul udløbne poster</string>
<string name="hide_expired_entries_summary">Udløbne poster er skjult</string> <string name="hide_expired_entries_summary">Udløbne poster er skjult</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>

View File

@@ -67,7 +67,7 @@
<string name="entry_user_name">Benutzername</string> <string name="entry_user_name">Benutzername</string>
<string name="error_arc4">Die RC4/Arcfour-Stromverschlüsselung wird nicht unterstützt.</string> <string name="error_arc4">Die RC4/Arcfour-Stromverschlüsselung wird nicht unterstützt.</string>
<string name="error_can_not_handle_uri">KeePassDX kann diese URI-Adresse nicht verarbeiten.</string> <string name="error_can_not_handle_uri">KeePassDX kann diese URI-Adresse nicht verarbeiten.</string>
<string name="error_file_not_create">Konnte Datei nicht erstellen:</string> <string name="error_file_not_create">Konnte Datei nicht erstellen</string>
<string name="error_invalid_db">Datenbank nicht lesbar.</string> <string name="error_invalid_db">Datenbank nicht lesbar.</string>
<string name="error_invalid_path">Sicherstellen, dass der Pfad korrekt ist.</string> <string name="error_invalid_path">Sicherstellen, dass der Pfad korrekt ist.</string>
<string name="error_no_name">Namen eingeben.</string> <string name="error_no_name">Namen eingeben.</string>
@@ -435,7 +435,7 @@
<string name="database_custom_color_title">Benutzerdefinierte Datenbankfarbe</string> <string name="database_custom_color_title">Benutzerdefinierte Datenbankfarbe</string>
<string name="compression">Kompression</string> <string name="compression">Kompression</string>
<string name="compression_none">Keine</string> <string name="compression_none">Keine</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Gerätetastatur-Einstellungen</string> <string name="device_keyboard_setting_title">Gerätetastatur-Einstellungen</string>
<string name="error_save_database">Die Datenbank konnte nicht gespeichert werden.</string> <string name="error_save_database">Die Datenbank konnte nicht gespeichert werden.</string>
<string name="menu_save_database">Datenbank speichern</string> <string name="menu_save_database">Datenbank speichern</string>
@@ -456,7 +456,7 @@
<string name="download_initialization">Initialisieren…</string> <string name="download_initialization">Initialisieren…</string>
<string name="download_progression">Fortschritt: %1$d%%</string> <string name="download_progression">Fortschritt: %1$d%%</string>
<string name="download_finalization">Fertigstellen…</string> <string name="download_finalization">Fertigstellen…</string>
<string name="download_complete">Vollständig! Tippen Sie, um die Datei zu öffnen.</string> <string name="download_complete">Vollständig!</string>
<string name="hide_expired_entries_title">Abgelaufene Einträge ausblenden</string> <string name="hide_expired_entries_title">Abgelaufene Einträge ausblenden</string>
<string name="hide_expired_entries_summary">Abgelaufene Einträge werden ausgeblendet</string> <string name="hide_expired_entries_summary">Abgelaufene Einträge werden ausgeblendet</string>
<string name="style_choose_title">App-Design</string> <string name="style_choose_title">App-Design</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Όνομα Χρήστη</string> <string name="entry_user_name">Όνομα Χρήστη</string>
<string name="error_arc4">Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται.</string> <string name="error_arc4">Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται.</string>
<string name="error_can_not_handle_uri">Το KeePassDX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI.</string> <string name="error_can_not_handle_uri">Το KeePassDX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI.</string>
<string name="error_file_not_create">Δεν ήταν δυνατή η δημιουργία αρχείου:</string> <string name="error_file_not_create">Δεν ήταν δυνατή η δημιουργία αρχείου</string>
<string name="error_invalid_db">Δεν ήταν δυνατή η ανάγνωση της βάσης δεδομένων.</string> <string name="error_invalid_db">Δεν ήταν δυνατή η ανάγνωση της βάσης δεδομένων.</string>
<string name="error_invalid_path">Βεβαιωθείτε ότι η διαδρομή είναι σωστή.</string> <string name="error_invalid_path">Βεβαιωθείτε ότι η διαδρομή είναι σωστή.</string>
<string name="error_no_name">Εισαγάγετε ένα όνομα.</string> <string name="error_no_name">Εισαγάγετε ένα όνομα.</string>
@@ -418,7 +418,7 @@
<string name="database_custom_color_title">Προσαρμοσμένο χρώμα βάσης δεδομένων</string> <string name="database_custom_color_title">Προσαρμοσμένο χρώμα βάσης δεδομένων</string>
<string name="compression">Συμπίεση</string> <string name="compression">Συμπίεση</string>
<string name="compression_none">Καμιά</string> <string name="compression_none">Καμιά</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="magic_keyboard_explanation_summary">Ενεργοποιώντας ένα προσαρμοσμένο πληκτρολόγιο συγκεντρώνει τους κωδικούς πρόσβασής σας και όλα τα πεδία ταυτότητας</string> <string name="magic_keyboard_explanation_summary">Ενεργοποιώντας ένα προσαρμοσμένο πληκτρολόγιο συγκεντρώνει τους κωδικούς πρόσβασής σας και όλα τα πεδία ταυτότητας</string>
<string name="device_keyboard_setting_title">Ρυθμίσεις πληκτρολογίου συσκευής</string> <string name="device_keyboard_setting_title">Ρυθμίσεις πληκτρολογίου συσκευής</string>
<string name="education_biometric_title">Ξεκλείδωμα Βάσης Δεδομένων με βιομετρικά στοιχεία</string> <string name="education_biometric_title">Ξεκλείδωμα Βάσης Δεδομένων με βιομετρικά στοιχεία</string>
@@ -442,7 +442,7 @@
<string name="download_initialization">Αρχικοποίηση…</string> <string name="download_initialization">Αρχικοποίηση…</string>
<string name="download_progression">Σε εξέλιξη: %1$d%%</string> <string name="download_progression">Σε εξέλιξη: %1$d%%</string>
<string name="download_finalization">Ολοκλήρωση…</string> <string name="download_finalization">Ολοκλήρωση…</string>
<string name="download_complete">Ολοκληρώθηκε! Πατήστε για να ανοίξετε το αρχείο.</string> <string name="download_complete">Ολοκληρώθηκε!</string>
<string name="hide_expired_entries_title">Απόκρυψη καταχωρίσεων που έχουν λήξει</string> <string name="hide_expired_entries_title">Απόκρυψη καταχωρίσεων που έχουν λήξει</string>
<string name="hide_expired_entries_summary">Οι καταχωρίσεις που έχουν λήξει είναι κρυμμένες</string> <string name="hide_expired_entries_summary">Οι καταχωρίσεις που έχουν λήξει είναι κρυμμένες</string>
<string name="show_recent_files_title">Εμφάνιση πρόσφατων αρχείων</string> <string name="show_recent_files_title">Εμφάνιση πρόσφατων αρχείων</string>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Nombre de usuario</string> <string name="entry_user_name">Nombre de usuario</string>
<string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string> <string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string>
<string name="error_can_not_handle_uri">KeePassDX no puede manejar este URI.</string> <string name="error_can_not_handle_uri">KeePassDX no puede manejar este URI.</string>
<string name="error_file_not_create">No se pudo crear el archivo:</string> <string name="error_file_not_create">No se pudo crear el archivo</string>
<string name="error_invalid_db">No se pudo leer la base de datos.</string> <string name="error_invalid_db">No se pudo leer la base de datos.</string>
<string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string> <string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string>
<string name="error_no_name">Proporcione un nombre.</string> <string name="error_no_name">Proporcione un nombre.</string>
@@ -397,7 +397,7 @@
<string name="error_create_database">No fue posible crear el archivo de base de datos.</string> <string name="error_create_database">No fue posible crear el archivo de base de datos.</string>
<string name="html_about_contribution">Parar lograr &lt;strong&gt;mantener nuestra libertad&lt;/strong&gt;, &lt;strong&gt;corregir errores&lt;/strong&gt;, &lt;strong&gt;agregar características&lt;/strong&gt; y &lt;strong&gt;siempre estar activos&lt;/strong&gt;, contamos con tu &lt;strong&gt;contribución&lt;/strong&gt;.</string> <string name="html_about_contribution">Parar lograr &lt;strong&gt;mantener nuestra libertad&lt;/strong&gt;, &lt;strong&gt;corregir errores&lt;/strong&gt;, &lt;strong&gt;agregar características&lt;/strong&gt; y &lt;strong&gt;siempre estar activos&lt;/strong&gt;, contamos con tu &lt;strong&gt;contribución&lt;/strong&gt;.</string>
<string name="content_description_add_item">Añadir elemento</string> <string name="content_description_add_item">Añadir elemento</string>
<string name="download_complete">Descarga completa! Toca para abrir el archivo.</string> <string name="download_complete">Descarga completa!</string>
<string name="download_finalization">Finalizando…</string> <string name="download_finalization">Finalizando…</string>
<string name="download_progression">En progreso: %1$d%%</string> <string name="download_progression">En progreso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string> <string name="download_initialization">Inicializando…</string>
@@ -407,7 +407,7 @@
<string name="autofill_block">Bloquear autocompletado</string> <string name="autofill_block">Bloquear autocompletado</string>
<string name="keyboard_change">Cambiar teclado</string> <string name="keyboard_change">Cambiar teclado</string>
<string name="keyboard_auto_go_action_summary">Acción de la tecla \"Ir\" al presionar una tecla \"Campo\"</string> <string name="keyboard_auto_go_action_summary">Acción de la tecla \"Ir\" al presionar una tecla \"Campo\"</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="compression_none">Ninguna</string> <string name="compression_none">Ninguna</string>
<string name="compression">Compresión</string> <string name="compression">Compresión</string>
<string name="database_default_username_title">Nombre de usuario predeterminado</string> <string name="database_default_username_title">Nombre de usuario predeterminado</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Erabiltzaile izena</string> <string name="entry_user_name">Erabiltzaile izena</string>
<string name="error_arc4">Arcfour stream zifratze sisterako ez dago euskarririk..</string> <string name="error_arc4">Arcfour stream zifratze sisterako ez dago euskarririk..</string>
<string name="error_can_not_handle_uri">KeePassDX-ek ezin dut uri hau kudeatu.</string> <string name="error_can_not_handle_uri">KeePassDX-ek ezin dut uri hau kudeatu.</string>
<string name="error_file_not_create">Ezin izan da fitxategia sortu:</string> <string name="error_file_not_create">Ezin izan da fitxategia sortu</string>
<string name="error_invalid_db">Datubase baliogabea.</string> <string name="error_invalid_db">Datubase baliogabea.</string>
<string name="error_invalid_path">Fitxategirako bide baliogabea.</string> <string name="error_invalid_path">Fitxategirako bide baliogabea.</string>
<string name="error_no_name">Izen bat behar da.</string> <string name="error_no_name">Izen bat behar da.</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Käyttäjänimi</string> <string name="entry_user_name">Käyttäjänimi</string>
<string name="error_arc4">Arcfour stream cipher ei ole tuettu.</string> <string name="error_arc4">Arcfour stream cipher ei ole tuettu.</string>
<string name="error_can_not_handle_uri">KeePassDX ei osaa käsitellä tätä osoitetta.</string> <string name="error_can_not_handle_uri">KeePassDX ei osaa käsitellä tätä osoitetta.</string>
<string name="error_file_not_create">Tiedoston luonti epäonnistui:</string> <string name="error_file_not_create">Tiedoston luonti epäonnistui</string>
<string name="error_invalid_db">Tietokantaa ei pystytty lukemaan.</string> <string name="error_invalid_db">Tietokantaa ei pystytty lukemaan.</string>
<string name="error_invalid_path">Varmista että polku on oikein.</string> <string name="error_invalid_path">Varmista että polku on oikein.</string>
<string name="error_no_name">Anna nimi.</string> <string name="error_no_name">Anna nimi.</string>

View File

@@ -65,7 +65,7 @@
<string name="entry_user_name">Nom dutilisateur</string> <string name="entry_user_name">Nom dutilisateur</string>
<string name="error_arc4">Le chiffrement de flux Arcfour nest pas pris en charge.</string> <string name="error_arc4">Le chiffrement de flux Arcfour nest pas pris en charge.</string>
<string name="error_can_not_handle_uri">Impossible de gérer cette URI dans KeePassDX.</string> <string name="error_can_not_handle_uri">Impossible de gérer cette URI dans KeePassDX.</string>
<string name="error_file_not_create">Impossible de créer le fichier :</string> <string name="error_file_not_create">Impossible de créer le fichier</string>
<string name="error_invalid_db">Impossible de lire la base de données.</string> <string name="error_invalid_db">Impossible de lire la base de données.</string>
<string name="error_invalid_path">Vérifier la validité du chemin daccès.</string> <string name="error_invalid_path">Vérifier la validité du chemin daccès.</string>
<string name="error_no_name">Saisir un nom.</string> <string name="error_no_name">Saisir un nom.</string>
@@ -435,7 +435,7 @@
<string name="database_custom_color_title">Couleur de la base de données</string> <string name="database_custom_color_title">Couleur de la base de données</string>
<string name="compression">Compression</string> <string name="compression">Compression</string>
<string name="compression_none">Aucune</string> <string name="compression_none">Aucune</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Paramètres du clavier de lappareil</string> <string name="device_keyboard_setting_title">Paramètres du clavier de lappareil</string>
<string name="error_save_database">Impossible denregistrer la base de données.</string> <string name="error_save_database">Impossible denregistrer la base de données.</string>
<string name="menu_save_database">Enregistrer la base de données</string> <string name="menu_save_database">Enregistrer la base de données</string>
@@ -456,7 +456,7 @@
<string name="download_initialization">Initialisation…</string> <string name="download_initialization">Initialisation…</string>
<string name="download_progression">En cours : %1$d%%</string> <string name="download_progression">En cours : %1$d%%</string>
<string name="download_finalization">Finalisation…</string> <string name="download_finalization">Finalisation…</string>
<string name="download_complete">Terminé ! Appuyer pour ouvrir le fichier.</string> <string name="download_complete">Terminé !</string>
<string name="hide_expired_entries_title">Masquer les entrées expirées</string> <string name="hide_expired_entries_title">Masquer les entrées expirées</string>
<string name="hide_expired_entries_summary">Les entrées expirées sont cachées</string> <string name="hide_expired_entries_summary">Les entrées expirées sont cachées</string>
<string name="contact">Contact</string> <string name="contact">Contact</string>

View File

@@ -62,7 +62,7 @@
<string name="entry_url">यू.आर.एल</string> <string name="entry_url">यू.आर.एल</string>
<string name="entry_user_name">उपयोगकर्ता का नाम</string> <string name="entry_user_name">उपयोगकर्ता का नाम</string>
<string name="error_can_not_handle_uri">KeePassDX में इस URI को संभाल नहीं सका।</string> <string name="error_can_not_handle_uri">KeePassDX में इस URI को संभाल नहीं सका।</string>
<string name="error_file_not_create">फाइल नहीं बना सका:</string> <string name="error_file_not_create">फाइल नहीं बना सका</string>
<string name="error_invalid_db">डाटाबेस नहीं पढ़ सका।</string> <string name="error_invalid_db">डाटाबेस नहीं पढ़ सका।</string>
<string name="error_invalid_path">सुनिश्चित करें कि रास्ता सही है।</string> <string name="error_invalid_path">सुनिश्चित करें कि रास्ता सही है।</string>
<string name="error_no_name">एक नाम दर्ज करें।</string> <string name="error_no_name">एक नाम दर्ज करें।</string>

View File

@@ -233,12 +233,12 @@
<string name="other">Ostalo</string> <string name="other">Ostalo</string>
<string name="compression">Komprimiranje</string> <string name="compression">Komprimiranje</string>
<string name="compression_none">Bez</string> <string name="compression_none">Bez</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="recycle_bin">Koš za smeće</string> <string name="recycle_bin">Koš za smeće</string>
<string name="content_description_node_children">Pod-čvor</string> <string name="content_description_node_children">Pod-čvor</string>
<string name="entry_accessed">Pristupljeno</string> <string name="entry_accessed">Pristupljeno</string>
<string name="error_arc4">Arcfour šifriranje nije podržano.</string> <string name="error_arc4">Arcfour šifriranje nije podržano.</string>
<string name="error_file_not_create">Nije moguće stvoriti datoteku:</string> <string name="error_file_not_create">Nije moguće stvoriti datoteku</string>
<string name="error_invalid_db">Nije moguće čitati bazu podataka.</string> <string name="error_invalid_db">Nije moguće čitati bazu podataka.</string>
<string name="error_invalid_path">Provjeri putanju do datoteke.</string> <string name="error_invalid_path">Provjeri putanju do datoteke.</string>
<string name="error_invalid_OTP">Neispravan OTP tajni ključ.</string> <string name="error_invalid_OTP">Neispravan OTP tajni ključ.</string>
@@ -469,7 +469,7 @@
\n \n
\nGrupe (~mape) organiziraju unose u bazi podataka.</string> \nGrupe (~mape) organiziraju unose u bazi podataka.</string>
<string name="download_progression">U tijeku: %1$d%%</string> <string name="download_progression">U tijeku: %1$d%%</string>
<string name="download_complete">Gotovo! Dodirni, za otvaranje datoteke.</string> <string name="download_complete">Gotovo!</string>
<string name="keyboard_previous_fill_in_summary">Automatski se vrati na prethodnu tipkovnicu nakon izvršavanja automatske radnje tipke</string> <string name="keyboard_previous_fill_in_summary">Automatski se vrati na prethodnu tipkovnicu nakon izvršavanja automatske radnje tipke</string>
<string name="keyboard_previous_fill_in_title">Automatska radnja tipke</string> <string name="keyboard_previous_fill_in_title">Automatska radnja tipke</string>
<string name="keyboard_previous_database_credentials_summary">Automatski se prebaci na prethodnu tipkovnicu pri ekranu za unos podataka za prijavu u bazu podataka</string> <string name="keyboard_previous_database_credentials_summary">Automatski se prebaci na prethodnu tipkovnicu pri ekranu za unos podataka za prijavu u bazu podataka</string>

View File

@@ -60,7 +60,7 @@
<string name="entry_user_name">Felhasználónév</string> <string name="entry_user_name">Felhasználónév</string>
<string name="error_arc4">Az Arcfour adatfolyam-titkosítás nem támogatott.</string> <string name="error_arc4">Az Arcfour adatfolyam-titkosítás nem támogatott.</string>
<string name="error_can_not_handle_uri">Ez az URI nem kezelhető a KeePassDX-ben.</string> <string name="error_can_not_handle_uri">Ez az URI nem kezelhető a KeePassDX-ben.</string>
<string name="error_file_not_create">Nem sikerült létrehozni a fájlt:</string> <string name="error_file_not_create">Nem sikerült létrehozni a fájlt</string>
<string name="error_invalid_db">Az adatbázist nem lehet olvasni.</string> <string name="error_invalid_db">Az adatbázist nem lehet olvasni.</string>
<string name="error_invalid_path">Győződjön meg róla, hogy az útvonal helyes.</string> <string name="error_invalid_path">Győződjön meg róla, hogy az útvonal helyes.</string>
<string name="error_no_name">Adjon meg egy nevet.</string> <string name="error_no_name">Adjon meg egy nevet.</string>
@@ -384,7 +384,7 @@
<string name="contact">Kapcsolat</string> <string name="contact">Kapcsolat</string>
<string name="hide_expired_entries_summary">A lejárt bejegyzések rejtettek</string> <string name="hide_expired_entries_summary">A lejárt bejegyzések rejtettek</string>
<string name="hide_expired_entries_title">Lejárt bejegyzések elrejtése</string> <string name="hide_expired_entries_title">Lejárt bejegyzések elrejtése</string>
<string name="download_complete">Kész! Koppintson a fájl megnyitásához.</string> <string name="download_complete">Kész!</string>
<string name="download_finalization">Befejezés…</string> <string name="download_finalization">Befejezés…</string>
<string name="download_progression">Folyamatban: %1$d%%</string> <string name="download_progression">Folyamatban: %1$d%%</string>
<string name="download_initialization">Előkészítés…</string> <string name="download_initialization">Előkészítés…</string>
@@ -405,7 +405,7 @@
<string name="menu_save_database">Adatbázis mentése</string> <string name="menu_save_database">Adatbázis mentése</string>
<string name="error_save_database">Az adatbázis mentése sikertelen.</string> <string name="error_save_database">Az adatbázis mentése sikertelen.</string>
<string name="device_keyboard_setting_title">Eszköz billentyűzetének beállításai</string> <string name="device_keyboard_setting_title">Eszköz billentyűzetének beállításai</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="compression_none">Nincs</string> <string name="compression_none">Nincs</string>
<string name="compression">Tömörítés</string> <string name="compression">Tömörítés</string>
<string name="database_custom_color_title">Egyéni adatbázisszín</string> <string name="database_custom_color_title">Egyéni adatbázisszín</string>

View File

@@ -63,7 +63,7 @@
<string name="entry_user_name">Nome utente</string> <string name="entry_user_name">Nome utente</string>
<string name="error_arc4">La codifica a flusso Arcfour non è supportata.</string> <string name="error_arc4">La codifica a flusso Arcfour non è supportata.</string>
<string name="error_can_not_handle_uri">KeePassDX non può gestire questo URI.</string> <string name="error_can_not_handle_uri">KeePassDX non può gestire questo URI.</string>
<string name="error_file_not_create">Impossibile creare il file:</string> <string name="error_file_not_create">Impossibile creare il file</string>
<string name="error_invalid_db">Lettura del database fallita.</string> <string name="error_invalid_db">Lettura del database fallita.</string>
<string name="error_invalid_path">Assicurati che il percorso sia corretto.</string> <string name="error_invalid_path">Assicurati che il percorso sia corretto.</string>
<string name="error_no_name">Inserisci un nome.</string> <string name="error_no_name">Inserisci un nome.</string>
@@ -448,7 +448,7 @@
<string name="hide_expired_entries_summary">I record scaduti sono nascosti</string> <string name="hide_expired_entries_summary">I record scaduti sono nascosti</string>
<string name="hide_expired_entries_title">Nascondi i record scaduti</string> <string name="hide_expired_entries_title">Nascondi i record scaduti</string>
<string name="education_setup_OTP_summary">Imposta la gestione delle OTP (HOTP / TOTP) per generare un token richiesto per la 2FA.</string> <string name="education_setup_OTP_summary">Imposta la gestione delle OTP (HOTP / TOTP) per generare un token richiesto per la 2FA.</string>
<string name="download_complete">Completo! Tocca per aprire il file.</string> <string name="download_complete">Completo!</string>
<string name="download_finalization">Finalizzazione…</string> <string name="download_finalization">Finalizzazione…</string>
<string name="download_progression">Avanzamento %1$d%%</string> <string name="download_progression">Avanzamento %1$d%%</string>
<string name="download_initialization">Inizializzazione…</string> <string name="download_initialization">Inizializzazione…</string>
@@ -461,7 +461,7 @@
<string name="keyboard_auto_go_action_summary">Dopo la pressione del tasto \"Campo\" invia il tasto \"Vai\"</string> <string name="keyboard_auto_go_action_summary">Dopo la pressione del tasto \"Campo\" invia il tasto \"Vai\"</string>
<string name="keyboard_auto_go_action_title">Azione auto key</string> <string name="keyboard_auto_go_action_title">Azione auto key</string>
<string name="device_keyboard_setting_title">Impostazioni tastiera dispositivo</string> <string name="device_keyboard_setting_title">Impostazioni tastiera dispositivo</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="compression_none">Nessuna</string> <string name="compression_none">Nessuna</string>
<string name="compression">Compressione</string> <string name="compression">Compressione</string>
<string name="database_custom_color_title">Colore del database customizzato</string> <string name="database_custom_color_title">Colore del database customizzato</string>

View File

@@ -60,7 +60,7 @@
<string name="entry_user_name">שם משתמש</string> <string name="entry_user_name">שם משתמש</string>
<string name="error_arc4">צופן זרם Arcfour אינו נתמך.</string> <string name="error_arc4">צופן זרם Arcfour אינו נתמך.</string>
<string name="error_can_not_handle_uri">KeePassDX לא יכול לטפל ב-URI הזה.</string> <string name="error_can_not_handle_uri">KeePassDX לא יכול לטפל ב-URI הזה.</string>
<string name="error_file_not_create">לא הצליח ליצור קובץ:</string> <string name="error_file_not_create">לא הצליח ליצור קובץ</string>
<string name="error_invalid_db">מסד נתונים לא חוקי.</string> <string name="error_invalid_db">מסד נתונים לא חוקי.</string>
<string name="error_invalid_path">נתיב לא חוקי.</string> <string name="error_invalid_path">נתיב לא חוקי.</string>
<string name="error_no_name">שם נדרש.</string> <string name="error_no_name">שם נדרש.</string>

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">ユーザー名</string> <string name="entry_user_name">ユーザー名</string>
<string name="error_arc4">Arcfour ストリーム暗号には対応していません。</string> <string name="error_arc4">Arcfour ストリーム暗号には対応していません。</string>
<string name="error_can_not_handle_uri">KeePassDX ではこの URI を処理できませんでした。</string> <string name="error_can_not_handle_uri">KeePassDX ではこの URI を処理できませんでした。</string>
<string name="error_file_not_create">ファイルを作成できませんでした</string> <string name="error_file_not_create">ファイルを作成できませんでした</string>
<string name="error_invalid_db">データベースを読み取れませんでした。</string> <string name="error_invalid_db">データベースを読み取れませんでした。</string>
<string name="error_invalid_path">パスが正しいことを確認してください。</string> <string name="error_invalid_path">パスが正しいことを確認してください。</string>
<string name="error_no_name">名前を入力してください。</string> <string name="error_no_name">名前を入力してください。</string>
@@ -407,7 +407,7 @@
<string name="clipboard_explanation_summary">デバイスのクリップボードを使用して、エントリーのフィールドをコピーします</string> <string name="clipboard_explanation_summary">デバイスのクリップボードを使用して、エントリーのフィールドをコピーします</string>
<string name="html_text_dev_feature_work_hard">この機能をすばやくリリースするために開発に勤しんでいます。</string> <string name="html_text_dev_feature_work_hard">この機能をすばやくリリースするために開発に勤しんでいます。</string>
<string name="magic_keyboard_explanation_summary">パスワードとすべての ID フィールドを格納するカスタム キーボードを有効にします</string> <string name="magic_keyboard_explanation_summary">パスワードとすべての ID フィールドを格納するカスタム キーボードを有効にします</string>
<string name="download_complete">完了しました!タップするとファイルが開きます。</string> <string name="download_complete">完了しました!</string>
<string name="download_progression">進行中:%1$d%%</string> <string name="download_progression">進行中:%1$d%%</string>
<string name="download_attachment">%1$s をダウンロード</string> <string name="download_attachment">%1$s をダウンロード</string>
<string name="contribute">貢献</string> <string name="contribute">貢献</string>
@@ -422,7 +422,7 @@
<string name="allow_no_password_title">空のマスターキーを許可</string> <string name="allow_no_password_title">空のマスターキーを許可</string>
<string name="autofill_auto_search_title">自動検索</string> <string name="autofill_auto_search_title">自動検索</string>
<string name="keyboard_label">Magikeyboard (KeePassDX)</string> <string name="keyboard_label">Magikeyboard (KeePassDX)</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="compression_none">なし</string> <string name="compression_none">なし</string>
<string name="compression">圧縮</string> <string name="compression">圧縮</string>
<string name="other">その他</string> <string name="other">その他</string>

View File

@@ -64,7 +64,7 @@
<string name="entry_user_name">아이디</string> <string name="entry_user_name">아이디</string>
<string name="error_arc4">Arcfour 스트림 암호는 지원되지 않습니다.</string> <string name="error_arc4">Arcfour 스트림 암호는 지원되지 않습니다.</string>
<string name="error_can_not_handle_uri">KeePassDX에서는 이 URI를 처리할 수 없습니다.</string> <string name="error_can_not_handle_uri">KeePassDX에서는 이 URI를 처리할 수 없습니다.</string>
<string name="error_file_not_create">파일을 생성할 수 없음:</string> <string name="error_file_not_create">파일을 생성할 수 없음</string>
<string name="error_invalid_db">데이터베이스를 읽을 수 없음.</string> <string name="error_invalid_db">데이터베이스를 읽을 수 없음.</string>
<string name="error_invalid_path">경로가 확실한지 확인하십시오.</string> <string name="error_invalid_path">경로가 확실한지 확인하십시오.</string>
<string name="error_no_name">이름을 입력하십시오.</string> <string name="error_no_name">이름을 입력하십시오.</string>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Lietotāja vārds</string> <string name="entry_user_name">Lietotāja vārds</string>
<string name="error_arc4">Arcfour plūsmas šifrs netiek atbalstīts.</string> <string name="error_arc4">Arcfour plūsmas šifrs netiek atbalstīts.</string>
<string name="error_can_not_handle_uri">Neizdevās pātiet uz norādīto adresi.</string> <string name="error_can_not_handle_uri">Neizdevās pātiet uz norādīto adresi.</string>
<string name="error_file_not_create">Neizdevās izveidot failu:</string> <string name="error_file_not_create">Neizdevās izveidot failu</string>
<string name="error_invalid_db">Nederīga datu bāze.</string> <string name="error_invalid_db">Nederīga datu bāze.</string>
<string name="error_invalid_path">Nederīgs ceļš.</string> <string name="error_invalid_path">Nederīgs ceļš.</string>
<string name="error_no_name">Vajag ievadīt faila nosaukumu</string> <string name="error_no_name">Vajag ievadīt faila nosaukumu</string>

View File

@@ -76,7 +76,7 @@
<string name="error_save_database">ഡാറ്റാബേസ് സംരക്ഷിക്കാൻ കഴിഞ്ഞില്ല.</string> <string name="error_save_database">ഡാറ്റാബേസ് സംരക്ഷിക്കാൻ കഴിഞ്ഞില്ല.</string>
<string name="error_pass_match">പാസ്‌വേഡുകൾ പൊരുത്തപ്പെടുന്നില്ല.</string> <string name="error_pass_match">പാസ്‌വേഡുകൾ പൊരുത്തപ്പെടുന്നില്ല.</string>
<string name="error_no_name">ഒരു പേര് നൽകുക.</string> <string name="error_no_name">ഒരു പേര് നൽകുക.</string>
<string name="error_file_not_create">ഫയൽ സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല:</string> <string name="error_file_not_create">ഫയൽ സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല</string>
<string name="error_invalid_db">ഡാറ്റാബേസ് വായിക്കാൻ സാധിച്ചില്ല.</string> <string name="error_invalid_db">ഡാറ്റാബേസ് വായിക്കാൻ സാധിച്ചില്ല.</string>
<string name="entry_user_name">ഉപയോക്തൃനാമം</string> <string name="entry_user_name">ഉപയോക്തൃനാമം</string>
<string name="entry_url">URL</string> <string name="entry_url">URL</string>
@@ -154,12 +154,12 @@
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>
<string name="encryption_twofish">Twofish</string> <string name="encryption_twofish">Twofish</string>
<string name="encryption_rijndael">Rijndael (AES)</string> <string name="encryption_rijndael">Rijndael (AES)</string>
<string name="download_complete">പൂർത്തിയാക്കി! ഫയൽ തുറക്കാൻ സ്പർശിക്കുക</string> <string name="download_complete">പൂർത്തിയാക്കി!</string>
<string name="education_create_database_title">നിങ്ങളുടെ ഡാറ്റാബേസ് ഫയൽ സൃഷ്ടിക്കുക</string> <string name="education_create_database_title">നിങ്ങളുടെ ഡാറ്റാബേസ് ഫയൽ സൃഷ്ടിക്കുക</string>
<string name="autofill_auto_search_title">സ്വയം തിരയൽ</string> <string name="autofill_auto_search_title">സ്വയം തിരയൽ</string>
<string name="keyboard_change">കീബോർഡ് മാറ്റുക</string> <string name="keyboard_change">കീബോർഡ് മാറ്റുക</string>
<string name="keyboard_label">Magikeyboard (KeePassDX)</string> <string name="keyboard_label">Magikeyboard (KeePassDX)</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="database_version_title">"ഡാറ്റാബ്‌സിൻ്റെ പതിപ്പ്"</string> <string name="database_version_title">"ഡാറ്റാബ്‌സിൻ്റെ പതിപ്പ്"</string>
<string name="max_history_items_title">പരമാവധി നമ്പർ</string> <string name="max_history_items_title">പരമാവധി നമ്പർ</string>
<string name="recycle_bin_title">റീസൈക്കിൾ ബിനിൻ്റെ ഉപയോഗം</string> <string name="recycle_bin_title">റീസൈക്കിൾ ബിനിൻ്റെ ഉപയോഗം</string>

View File

@@ -64,7 +64,7 @@
<string name="entry_user_name">Brukernavn</string> <string name="entry_user_name">Brukernavn</string>
<string name="error_arc4">Arcfour-strømchifferet støttes ikke.</string> <string name="error_arc4">Arcfour-strømchifferet støttes ikke.</string>
<string name="error_can_not_handle_uri">KeePassDX kan ikke håntere denne URI-en.</string> <string name="error_can_not_handle_uri">KeePassDX kan ikke håntere denne URI-en.</string>
<string name="error_file_not_create">Kunne ikke opprette fil:</string> <string name="error_file_not_create">Kunne ikke opprette fil</string>
<string name="error_invalid_db">Ugyldig database eller fremmed hovednøkkel.</string> <string name="error_invalid_db">Ugyldig database eller fremmed hovednøkkel.</string>
<string name="error_invalid_path">Ugyldig sti.</string> <string name="error_invalid_path">Ugyldig sti.</string>
<string name="error_no_name">Et navn er påkrevd.</string> <string name="error_no_name">Et navn er påkrevd.</string>
@@ -387,7 +387,7 @@
<string name="database_data_compression_summary">Datakomprimering reduserer databasens størrelse.</string> <string name="database_data_compression_summary">Datakomprimering reduserer databasens størrelse.</string>
<string name="compression">Komprimering</string> <string name="compression">Komprimering</string>
<string name="compression_none">Ingen</string> <string name="compression_none">Ingen</string>
<string name="compression_gzip">GZip</string> <string name="compression_gzip">Gzip</string>
<string name="error_save_database">Kunne ikke lagre database.</string> <string name="error_save_database">Kunne ikke lagre database.</string>
<string name="menu_empty_recycle_bin">Tøm papirkurven</string> <string name="menu_empty_recycle_bin">Tøm papirkurven</string>
<string name="command_execution">Kjører kommandoen…</string> <string name="command_execution">Kjører kommandoen…</string>
@@ -401,7 +401,7 @@
<string name="download_attachment">Last ned %1$s</string> <string name="download_attachment">Last ned %1$s</string>
<string name="download_progression">Underveis: %1$d%%</string> <string name="download_progression">Underveis: %1$d%%</string>
<string name="download_finalization">Fullfører…</string> <string name="download_finalization">Fullfører…</string>
<string name="download_complete">Fullført. Trykk for å åpne filen.</string> <string name="download_complete">Fullført!</string>
<string name="hide_expired_entries_title">Skjul utløpte oppføringer</string> <string name="hide_expired_entries_title">Skjul utløpte oppføringer</string>
<string name="auto_focus_search_title">Hurtigsøk</string> <string name="auto_focus_search_title">Hurtigsøk</string>
<string name="entry_add_attachment">Legg til vedlegg</string> <string name="entry_add_attachment">Legg til vedlegg</string>

View File

@@ -59,7 +59,7 @@
<string name="entry_user_name">Gebruikersnaam</string> <string name="entry_user_name">Gebruikersnaam</string>
<string name="error_arc4">De Arcfour stream-versleuteling wordt niet ondersteund.</string> <string name="error_arc4">De Arcfour stream-versleuteling wordt niet ondersteund.</string>
<string name="error_can_not_handle_uri">KeePassDX kan deze URI niet verwerken.</string> <string name="error_can_not_handle_uri">KeePassDX kan deze URI niet verwerken.</string>
<string name="error_file_not_create">Bestand is niet aangemaakt:</string> <string name="error_file_not_create">Bestand is niet aangemaakt</string>
<string name="error_invalid_db">Kan database niet uitlezen.</string> <string name="error_invalid_db">Kan database niet uitlezen.</string>
<string name="error_invalid_path">Zorg ervoor dat het pad juist is.</string> <string name="error_invalid_path">Zorg ervoor dat het pad juist is.</string>
<string name="error_no_name">Voer een naam in.</string> <string name="error_no_name">Voer een naam in.</string>
@@ -425,7 +425,7 @@
<string name="database_custom_color_title">Aangepaste databasekleur</string> <string name="database_custom_color_title">Aangepaste databasekleur</string>
<string name="compression">Compressie</string> <string name="compression">Compressie</string>
<string name="compression_none">Geen</string> <string name="compression_none">Geen</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Toetsenbordinstellingen</string> <string name="device_keyboard_setting_title">Toetsenbordinstellingen</string>
<string name="enable_auto_save_database_summary">Sla de database op na elke belangrijke actie (in \"Schrijf\" modus)</string> <string name="enable_auto_save_database_summary">Sla de database op na elke belangrijke actie (in \"Schrijf\" modus)</string>
<string name="education_setup_OTP_title">Instellingen OTP</string> <string name="education_setup_OTP_title">Instellingen OTP</string>
@@ -433,7 +433,7 @@
<string name="remember_database_locations_title">Databaselocatie opslaan</string> <string name="remember_database_locations_title">Databaselocatie opslaan</string>
<string name="hide_expired_entries_summary">Verlopen items worden verborgen</string> <string name="hide_expired_entries_summary">Verlopen items worden verborgen</string>
<string name="hide_expired_entries_title">Verberg verlopen items</string> <string name="hide_expired_entries_title">Verberg verlopen items</string>
<string name="download_complete">Klaar! Tik om het bestand te openen.</string> <string name="download_complete">Tik om het bestand te openen</string>
<string name="download_finalization">Voltooien…</string> <string name="download_finalization">Voltooien…</string>
<string name="download_progression">Voortgang: %1$d%%</string> <string name="download_progression">Voortgang: %1$d%%</string>
<string name="download_initialization">Initialiseren…</string> <string name="download_initialization">Initialiseren…</string>

View File

@@ -56,7 +56,7 @@
<string name="entry_user_name">Brukaramn</string> <string name="entry_user_name">Brukaramn</string>
<string name="error_arc4">Kan ikkje bruka Arcfour dataflytkryptering.</string> <string name="error_arc4">Kan ikkje bruka Arcfour dataflytkryptering.</string>
<string name="error_can_not_handle_uri">KeePassDX kan ikkje bruka denne ressursen.</string> <string name="error_can_not_handle_uri">KeePassDX kan ikkje bruka denne ressursen.</string>
<string name="error_file_not_create">Klarte ikkje å laga fila:</string> <string name="error_file_not_create">Klarte ikkje å laga fila</string>
<string name="error_invalid_db">Ugyldig database.</string> <string name="error_invalid_db">Ugyldig database.</string>
<string name="error_invalid_path">Ugyldig stig.</string> <string name="error_invalid_path">Ugyldig stig.</string>
<string name="error_no_name">Treng eit namn.</string> <string name="error_no_name">Treng eit namn.</string>

View File

@@ -6,7 +6,7 @@
<string name="icon_pack_choose_title">ਆਈਕਾਨ ਪੈਕ</string> <string name="icon_pack_choose_title">ਆਈਕਾਨ ਪੈਕ</string>
<string name="style_choose_summary">ਐਪ ਵਿੱਚ ਵਰਤਿਆ ਥੀਮ</string> <string name="style_choose_summary">ਐਪ ਵਿੱਚ ਵਰਤਿਆ ਥੀਮ</string>
<string name="style_choose_title">ਐਪ ਦਾ ਥੀਮ</string> <string name="style_choose_title">ਐਪ ਦਾ ਥੀਮ</string>
<string name="download_complete">ਪੂਰਾ ਹੋਇਆ! ਫ਼ਾਇਲ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string> <string name="download_complete">ਪੂਰਾ ਹੋਇਆ!</string>
<string name="download_finalization">…ਪੂਰਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string> <string name="download_finalization">…ਪੂਰਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
<string name="download_progression">ਜਾਰੀ ਹੈ: %1$d%%</string> <string name="download_progression">ਜਾਰੀ ਹੈ: %1$d%%</string>
<string name="download_initialization">…ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string> <string name="download_initialization">…ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
@@ -156,7 +156,7 @@
<string name="error_invalid_OTP">ਗ਼ਲਤ OTP ਭੇਤ ਹੈ।</string> <string name="error_invalid_OTP">ਗ਼ਲਤ OTP ਭੇਤ ਹੈ।</string>
<string name="error_invalid_path">ਪਾਥ ਦੇ ਠੀਕ ਹੋਣ ਨੂੰ ਯਕੀਨੀ ਬਣਾਓ।</string> <string name="error_invalid_path">ਪਾਥ ਦੇ ਠੀਕ ਹੋਣ ਨੂੰ ਯਕੀਨੀ ਬਣਾਓ।</string>
<string name="error_invalid_db">ਡਾਟਾਬੇਸ ਪੜ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।</string> <string name="error_invalid_db">ਡਾਟਾਬੇਸ ਪੜ੍ਹਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।</string>
<string name="error_file_not_create">ਫ਼ਾਇਲ ਬਣਾਈ ਨਹੀਂ ਜਾ ਸਕੀ:</string> <string name="error_file_not_create">ਫ਼ਾਇਲ ਬਣਾਈ ਨਹੀਂ ਜਾ ਸਕੀ</string>
<string name="error_can_not_handle_uri">ਇਹ URI KeePassDX ਵਿੱਚ ਹੈਂਡਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string> <string name="error_can_not_handle_uri">ਇਹ URI KeePassDX ਵਿੱਚ ਹੈਂਡਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ।</string>
<string name="error_arc4">Arcfour ਸਟਰੀਮ ਸੀਫ਼ਰ ਸਹਾਇਕ ਨਹੀਂ ਹੈ।</string> <string name="error_arc4">Arcfour ਸਟਰੀਮ ਸੀਫ਼ਰ ਸਹਾਇਕ ਨਹੀਂ ਹੈ।</string>
<string name="entry_user_name">ਵਰਤੋਂਕਾਰ-ਨਾਂ</string> <string name="entry_user_name">ਵਰਤੋਂਕਾਰ-ਨਾਂ</string>

View File

@@ -55,7 +55,7 @@
<string name="entry_user_name">Nazwa użytkownika</string> <string name="entry_user_name">Nazwa użytkownika</string>
<string name="error_arc4">Strumieniowe szyfrowanie Arcfour nie jest wspierane.</string> <string name="error_arc4">Strumieniowe szyfrowanie Arcfour nie jest wspierane.</string>
<string name="error_can_not_handle_uri">Nie można obsłużyć tego identyfikatora URI w KeePassDX.</string> <string name="error_can_not_handle_uri">Nie można obsłużyć tego identyfikatora URI w KeePassDX.</string>
<string name="error_file_not_create">Nie można utworzyć pliku:</string> <string name="error_file_not_create">Nie można utworzyć pliku</string>
<string name="error_invalid_db">Nie można odczytać bazy danych.</string> <string name="error_invalid_db">Nie można odczytać bazy danych.</string>
<string name="error_invalid_path">Upewnij się, że ścieżka jest prawidłowa.</string> <string name="error_invalid_path">Upewnij się, że ścieżka jest prawidłowa.</string>
<string name="error_no_name">Wpisz nazwę.</string> <string name="error_no_name">Wpisz nazwę.</string>
@@ -405,7 +405,7 @@
<string name="database_custom_color_title">Niestandardowy kolor bazy danych</string> <string name="database_custom_color_title">Niestandardowy kolor bazy danych</string>
<string name="compression">Kompresja</string> <string name="compression">Kompresja</string>
<string name="compression_none">Żaden</string> <string name="compression_none">Żaden</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Ustawienia klawiatury urządzenia</string> <string name="device_keyboard_setting_title">Ustawienia klawiatury urządzenia</string>
<string name="error_invalid_OTP">Nieprawidłowy klucz tajny OTP.</string> <string name="error_invalid_OTP">Nieprawidłowy klucz tajny OTP.</string>
<string name="error_disallow_no_credentials">Należy ustawić co najmniej jedno poświadczenie.</string> <string name="error_disallow_no_credentials">Należy ustawić co najmniej jedno poświadczenie.</string>
@@ -442,7 +442,7 @@
<string name="download_initialization">Inicjowanie…</string> <string name="download_initialization">Inicjowanie…</string>
<string name="download_progression">W trakcie realizacji: %1$d%%</string> <string name="download_progression">W trakcie realizacji: %1$d%%</string>
<string name="download_finalization">Kończę…</string> <string name="download_finalization">Kończę…</string>
<string name="download_complete">Kompletny! Stuknij, aby otworzyć plik.</string> <string name="download_complete">Kompletny!</string>
<string name="hide_expired_entries_title">Ukryj wygasłe wpisy</string> <string name="hide_expired_entries_title">Ukryj wygasłe wpisy</string>
<string name="hide_expired_entries_summary">Wygasłe wpisy są ukryte</string> <string name="hide_expired_entries_summary">Wygasłe wpisy są ukryte</string>
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>

View File

@@ -56,7 +56,7 @@
<string name="entry_user_name">Nome de usuário</string> <string name="entry_user_name">Nome de usuário</string>
<string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string> <string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string>
<string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string> <string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string>
<string name="error_file_not_create">Não foi possível criar o arquivo:</string> <string name="error_file_not_create">Não foi possível criar o arquivo</string>
<string name="error_invalid_db">Falha ao ler o banco.</string> <string name="error_invalid_db">Falha ao ler o banco.</string>
<string name="error_invalid_path">Certifique-se de que o caminho está correto.</string> <string name="error_invalid_path">Certifique-se de que o caminho está correto.</string>
<string name="error_no_name">Digite um nome.</string> <string name="error_no_name">Digite um nome.</string>
@@ -423,7 +423,7 @@
<string name="database_custom_color_title">Cor personalizada do banco de dados</string> <string name="database_custom_color_title">Cor personalizada do banco de dados</string>
<string name="compression">Compressão</string> <string name="compression">Compressão</string>
<string name="compression_none">Nada</string> <string name="compression_none">Nada</string>
<string name="compression_gzip">GZip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Configurações do teclado do aparelho</string> <string name="device_keyboard_setting_title">Configurações do teclado do aparelho</string>
<string name="error_save_database">Não foi possível salvar no banco de dados.</string> <string name="error_save_database">Não foi possível salvar no banco de dados.</string>
<string name="menu_save_database">Salvar banco de dados</string> <string name="menu_save_database">Salvar banco de dados</string>
@@ -443,7 +443,7 @@
<string name="autofill_auto_search_summary">Sugerir resultados de pesquisa de domínios da internet ou de aplicações automaticamente</string> <string name="autofill_auto_search_summary">Sugerir resultados de pesquisa de domínios da internet ou de aplicações automaticamente</string>
<string name="hide_expired_entries_summary">Entradas expeiradas foram escondidas</string> <string name="hide_expired_entries_summary">Entradas expeiradas foram escondidas</string>
<string name="hide_expired_entries_title">Esconder entradas expiradas</string> <string name="hide_expired_entries_title">Esconder entradas expiradas</string>
<string name="download_complete">Completo! Toque para abrir o aquivo.</string> <string name="download_complete">Completo!</string>
<string name="download_finalization">Finalizando…</string> <string name="download_finalization">Finalizando…</string>
<string name="download_progression">Em progresso: %1$d%%</string> <string name="download_progression">Em progresso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string> <string name="download_initialization">Inicializando…</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Nome de utilizador</string> <string name="entry_user_name">Nome de utilizador</string>
<string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string> <string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string>
<string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string> <string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string>
<string name="error_file_not_create">Não foi possível criar o ficheiro:</string> <string name="error_file_not_create">Não foi possível criar o ficheiro</string>
<string name="error_invalid_db">Não foi possível ler a base de dados.</string> <string name="error_invalid_db">Não foi possível ler a base de dados.</string>
<string name="error_invalid_path">Certifique-se que o caminho é válido.</string> <string name="error_invalid_path">Certifique-se que o caminho é válido.</string>
<string name="error_no_name">Introduza um nome.</string> <string name="error_no_name">Introduza um nome.</string>
@@ -420,7 +420,7 @@
<string name="html_about_contribution">Para &lt;strong&gt;manter a liberdade&lt;/strong&gt;, &lt;strong&gt;solucionar bugs&lt;/strong&gt;, &lt;strong&gt;adicionar funções&lt;/strong&gt; e &lt;strong&gt;para sermos sempre ativoa&lt;/strong&gt;, contamos com sua &lt;strong&gt;contribuição&lt;/strong&gt;.</string> <string name="html_about_contribution">Para &lt;strong&gt;manter a liberdade&lt;/strong&gt;, &lt;strong&gt;solucionar bugs&lt;/strong&gt;, &lt;strong&gt;adicionar funções&lt;/strong&gt; e &lt;strong&gt;para sermos sempre ativoa&lt;/strong&gt;, contamos com sua &lt;strong&gt;contribuição&lt;/strong&gt;.</string>
<string name="biometric_unlock_enable_title">Desbloqueio por biométrico</string> <string name="biometric_unlock_enable_title">Desbloqueio por biométrico</string>
<string name="entry_attachments">Anexos</string> <string name="entry_attachments">Anexos</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="discard">Descartar</string> <string name="discard">Descartar</string>
<string name="remember_database_locations_summary">Lembrar o local das bases de dados</string> <string name="remember_database_locations_summary">Lembrar o local das bases de dados</string>
<string name="auto_focus_search_summary">Solicitar uma pesquisa quando abrir a base de dados</string> <string name="auto_focus_search_summary">Solicitar uma pesquisa quando abrir a base de dados</string>
@@ -428,7 +428,7 @@
<string name="download_finalization">A finalizar…</string> <string name="download_finalization">A finalizar…</string>
<string name="autofill_preference_title">Configurações de preenchimento automático</string> <string name="autofill_preference_title">Configurações de preenchimento automático</string>
<string name="max_history_items_summary">Limitar a quantidade de itens do histórico por entrada</string> <string name="max_history_items_summary">Limitar a quantidade de itens do histórico por entrada</string>
<string name="download_complete">Completo! Toque para abrir o ficheiro.</string> <string name="download_complete">Completo!</string>
<string name="content_description_update_from_list">Atualizar</string> <string name="content_description_update_from_list">Atualizar</string>
<string name="contribution">Contribuição</string> <string name="contribution">Contribuição</string>
<string name="hide_broken_locations_summary">Esconder ligações quebradas na lista de bases de dados recentes</string> <string name="hide_broken_locations_summary">Esconder ligações quebradas na lista de bases de dados recentes</string>

View File

@@ -95,7 +95,7 @@
<string name="entry_user_name">Nume utilizator</string> <string name="entry_user_name">Nume utilizator</string>
<string name="error_arc4">Cifrarea fluxului Arcfour nu este acceptată.</string> <string name="error_arc4">Cifrarea fluxului Arcfour nu este acceptată.</string>
<string name="error_can_not_handle_uri">Nu s-a putut gestiona acest URI în KeePassDX.</string> <string name="error_can_not_handle_uri">Nu s-a putut gestiona acest URI în KeePassDX.</string>
<string name="error_file_not_create">Nu s-a putut creea fisierul:</string> <string name="error_file_not_create">Nu s-a putut creea fisierul</string>
<string name="error_invalid_db">Nu s-a putut citi baza de date.</string> <string name="error_invalid_db">Nu s-a putut citi baza de date.</string>
<string name="error_invalid_path">Asigurați-vă că calea este corectă.</string> <string name="error_invalid_path">Asigurați-vă că calea este corectă.</string>
<string name="error_invalid_OTP">Secret OTP nevalid.</string> <string name="error_invalid_OTP">Secret OTP nevalid.</string>
@@ -323,7 +323,7 @@
<string name="other">Alta</string> <string name="other">Alta</string>
<string name="compression">Compresie</string> <string name="compression">Compresie</string>
<string name="compression_none">Nimic</string> <string name="compression_none">Nimic</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="recycle_bin">Cos de reciclare</string> <string name="recycle_bin">Cos de reciclare</string>
<string name="keyboard">Tastatura</string> <string name="keyboard">Tastatura</string>
<string name="magic_keyboard_title">TastaturaMagica</string> <string name="magic_keyboard_title">TastaturaMagica</string>
@@ -418,7 +418,7 @@
<string name="download_initialization">Inițializare …</string> <string name="download_initialization">Inițializare …</string>
<string name="download_progression">In progress: %1$d%%</string> <string name="download_progression">In progress: %1$d%%</string>
<string name="download_finalization">Finalizare …</string> <string name="download_finalization">Finalizare …</string>
<string name="download_complete">Complet! Atingeți pentru a deschide fișierul.</string> <string name="download_complete">Complet!</string>
<string name="encryption_rijndael">Rijndael (AES)</string> <string name="encryption_rijndael">Rijndael (AES)</string>
<string name="encryption_twofish">Twofish</string> <string name="encryption_twofish">Twofish</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Имя пользователя</string> <string name="entry_user_name">Имя пользователя</string>
<string name="error_arc4">Потоковый шифр Arcfour не поддерживается.</string> <string name="error_arc4">Потоковый шифр Arcfour не поддерживается.</string>
<string name="error_can_not_handle_uri">Невозможно обработать указанный URI в KeePassDX.</string> <string name="error_can_not_handle_uri">Невозможно обработать указанный URI в KeePassDX.</string>
<string name="error_file_not_create">Невозможно создать файл:</string> <string name="error_file_not_create">Невозможно создать файл</string>
<string name="error_invalid_db">Невозможно прочитать базу.</string> <string name="error_invalid_db">Невозможно прочитать базу.</string>
<string name="error_invalid_path">Убедитесь, что путь указан правильно.</string> <string name="error_invalid_path">Убедитесь, что путь указан правильно.</string>
<string name="error_no_name">Введите название.</string> <string name="error_no_name">Введите название.</string>
@@ -421,7 +421,7 @@
<string name="database_custom_color_title">Произвольный цвет базы</string> <string name="database_custom_color_title">Произвольный цвет базы</string>
<string name="compression">Сжатие</string> <string name="compression">Сжатие</string>
<string name="compression_none">Нет</string> <string name="compression_none">Нет</string>
<string name="compression_gzip">GZip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Настройки клавиатур устройства</string> <string name="device_keyboard_setting_title">Настройки клавиатур устройства</string>
<string name="error_save_database">Невозможно сохранить базу.</string> <string name="error_save_database">Невозможно сохранить базу.</string>
<string name="menu_save_database">Сохранить базу</string> <string name="menu_save_database">Сохранить базу</string>
@@ -442,7 +442,7 @@
<string name="download_initialization">Инициализация…</string> <string name="download_initialization">Инициализация…</string>
<string name="download_progression">Выполнение: %1$d%%</string> <string name="download_progression">Выполнение: %1$d%%</string>
<string name="download_finalization">Завершение…</string> <string name="download_finalization">Завершение…</string>
<string name="download_complete">Готово! Нажмите, чтобы открыть файл.</string> <string name="download_complete">Готово!</string>
<string name="hide_expired_entries_title">Скрывать устаревшие записи</string> <string name="hide_expired_entries_title">Скрывать устаревшие записи</string>
<string name="hide_expired_entries_summary">Записи с истёкшим сроком окончания будут скрыты</string> <string name="hide_expired_entries_summary">Записи с истёкшим сроком окончания будут скрыты</string>
<string name="contact">Контактная информация</string> <string name="contact">Контактная информация</string>

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">Meno používateľa</string> <string name="entry_user_name">Meno používateľa</string>
<string name="error_arc4">Arcfour stream šifra nieje podporovaná.</string> <string name="error_arc4">Arcfour stream šifra nieje podporovaná.</string>
<string name="error_can_not_handle_uri">KeePassDX nevie použiť túto uri.</string> <string name="error_can_not_handle_uri">KeePassDX nevie použiť túto uri.</string>
<string name="error_file_not_create">Neviem vytvoriť súbor:</string> <string name="error_file_not_create">Neviem vytvoriť súbor</string>
<string name="error_invalid_db">Chybná databáza.</string> <string name="error_invalid_db">Chybná databáza.</string>
<string name="error_invalid_path">Chybná cesta.</string> <string name="error_invalid_path">Chybná cesta.</string>
<string name="error_no_name">Vyžaduje sa meno.</string> <string name="error_no_name">Vyžaduje sa meno.</string>

View File

@@ -60,7 +60,7 @@
<string name="entry_user_name">Användarnamn</string> <string name="entry_user_name">Användarnamn</string>
<string name="error_arc4">Strömchiffret Arcfour stöds inte.</string> <string name="error_arc4">Strömchiffret Arcfour stöds inte.</string>
<string name="error_can_not_handle_uri">KeePassDX kunde inte hantera denna URI.</string> <string name="error_can_not_handle_uri">KeePassDX kunde inte hantera denna URI.</string>
<string name="error_file_not_create">Kunde inte skapa filen:</string> <string name="error_file_not_create">Kunde inte skapa filen</string>
<string name="error_invalid_db">Kunde inte läsa databas.</string> <string name="error_invalid_db">Kunde inte läsa databas.</string>
<string name="error_invalid_path">Se till att sökvägen är korrekt.</string> <string name="error_invalid_path">Se till att sökvägen är korrekt.</string>
<string name="error_no_name">Ange ett namn.</string> <string name="error_no_name">Ange ett namn.</string>
@@ -418,7 +418,7 @@
<string name="database_custom_color_title">Anpassad databasfärg</string> <string name="database_custom_color_title">Anpassad databasfärg</string>
<string name="compression">Komprimering</string> <string name="compression">Komprimering</string>
<string name="compression_none">Ingen</string> <string name="compression_none">Ingen</string>
<string name="compression_gzip">GZip</string> <string name="compression_gzip">Gzip</string>
<string name="magic_keyboard_explanation_summary">Aktivera ett anpassat tangentbord som innehåller dina lösenord och alla identitetsfält</string> <string name="magic_keyboard_explanation_summary">Aktivera ett anpassat tangentbord som innehåller dina lösenord och alla identitetsfält</string>
<string name="device_keyboard_setting_title">Enhetens tangentbordsinställningar</string> <string name="device_keyboard_setting_title">Enhetens tangentbordsinställningar</string>
<string name="education_biometric_title">Lås upp databasen med biometrik</string> <string name="education_biometric_title">Lås upp databasen med biometrik</string>
@@ -446,7 +446,7 @@
<string name="contact">Kontakt</string> <string name="contact">Kontakt</string>
<string name="hide_expired_entries_summary">Utgångna poster är dolda</string> <string name="hide_expired_entries_summary">Utgångna poster är dolda</string>
<string name="hide_expired_entries_title">Dölj utgångna poster</string> <string name="hide_expired_entries_title">Dölj utgångna poster</string>
<string name="download_complete">Färdigt! Klicka för att öppna filen.</string> <string name="download_complete">Färdigt!</string>
<string name="download_finalization">Färdigställande…</string> <string name="download_finalization">Färdigställande…</string>
<string name="download_progression">Händelse %1$d%%</string> <string name="download_progression">Händelse %1$d%%</string>
<string name="download_initialization">Initiering…</string> <string name="download_initialization">Initiering…</string>

View File

@@ -53,7 +53,7 @@
<string name="entry_title">Başlık</string> <string name="entry_title">Başlık</string>
<string name="entry_url">URL</string> <string name="entry_url">URL</string>
<string name="entry_user_name">Kullanıcı adı</string> <string name="entry_user_name">Kullanıcı adı</string>
<string name="error_file_not_create">Dosya oluşturulamadı:</string> <string name="error_file_not_create">Dosya oluşturulamadı</string>
<string name="error_invalid_db">Veritabanı okunamadı.</string> <string name="error_invalid_db">Veritabanı okunamadı.</string>
<string name="error_invalid_path">Yolun doğru olduğundan emin olun.</string> <string name="error_invalid_path">Yolun doğru olduğundan emin olun.</string>
<string name="error_no_name">Bir isim girin.</string> <string name="error_no_name">Bir isim girin.</string>
@@ -405,7 +405,7 @@
<string name="database_custom_color_title">Özel veritabanı rengi</string> <string name="database_custom_color_title">Özel veritabanı rengi</string>
<string name="compression">Sıkıştırma</string> <string name="compression">Sıkıştırma</string>
<string name="compression_none">Yok</string> <string name="compression_none">Yok</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="device_keyboard_setting_title">Cihaz klavye ayarları</string> <string name="device_keyboard_setting_title">Cihaz klavye ayarları</string>
<string name="error_save_database">Veritabanı kaydedilemedi.</string> <string name="error_save_database">Veritabanı kaydedilemedi.</string>
<string name="menu_save_database">Veritabanını kaydet</string> <string name="menu_save_database">Veritabanını kaydet</string>
@@ -426,7 +426,7 @@
<string name="download_initialization">Başlatılıyor…</string> <string name="download_initialization">Başlatılıyor…</string>
<string name="download_progression">Devam ediyor: %1$d%%</string> <string name="download_progression">Devam ediyor: %1$d%%</string>
<string name="download_finalization">Sonlandırılıyor…</string> <string name="download_finalization">Sonlandırılıyor…</string>
<string name="download_complete">Tamamlandı! Dosyayı açmak için dokunun.</string> <string name="download_complete">Tamamlandı!</string>
<string name="hide_expired_entries_title">Süresi dolmuş girdileri gizle</string> <string name="hide_expired_entries_title">Süresi dolmuş girdileri gizle</string>
<string name="hide_expired_entries_summary">Süresi dolmuş girdiler gizlenecek</string> <string name="hide_expired_entries_summary">Süresi dolmuş girdiler gizlenecek</string>
<string name="warning_database_read_only">Veri tabanı değişikliklerini kaydetmek için dosya yazma erişimi ver</string> <string name="warning_database_read_only">Veri tabanı değişikliklerini kaydetmek için dosya yazma erişimi ver</string>

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">Ім’я користувача</string> <string name="entry_user_name">Ім’я користувача</string>
<string name="error_arc4">Потокове шифрування Arcfour не підтримується.</string> <string name="error_arc4">Потокове шифрування Arcfour не підтримується.</string>
<string name="error_can_not_handle_uri">Не вдалось обробити цей URI в KeePassDX.</string> <string name="error_can_not_handle_uri">Не вдалось обробити цей URI в KeePassDX.</string>
<string name="error_file_not_create">Не вдалося створити нотатку:</string> <string name="error_file_not_create">Не вдалося створити нотатку</string>
<string name="error_invalid_db">Неможливо прочитати базу даних.</string> <string name="error_invalid_db">Неможливо прочитати базу даних.</string>
<string name="error_invalid_path">Переконайтеся у правильності шляху.</string> <string name="error_invalid_path">Переконайтеся у правильності шляху.</string>
<string name="error_no_name">Введіть назву.</string> <string name="error_no_name">Введіть назву.</string>
@@ -152,7 +152,7 @@
<string name="content_description_add_group">Додати групу</string> <string name="content_description_add_group">Додати групу</string>
<string name="content_description_update_from_list">Оновити</string> <string name="content_description_update_from_list">Оновити</string>
<string name="keyboard">Клавіатура</string> <string name="keyboard">Клавіатура</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="compression">Стиснення</string> <string name="compression">Стиснення</string>
<string name="other">Інше</string> <string name="other">Інше</string>
<string name="application_appearance">Застосунок</string> <string name="application_appearance">Застосунок</string>
@@ -279,7 +279,7 @@
<string name="kdf_Argon2">Argon2</string> <string name="kdf_Argon2">Argon2</string>
<string name="kdf_AES">AES</string> <string name="kdf_AES">AES</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>
<string name="download_complete">Готово! Торкніться, щоб відкрити файл.</string> <string name="download_complete">Готово!</string>
<string name="download_finalization">Завершення…</string> <string name="download_finalization">Завершення…</string>
<string name="download_progression">Виконується: %1$d%%</string> <string name="download_progression">Виконується: %1$d%%</string>
<string name="download_initialization">Ініціалізація…</string> <string name="download_initialization">Ініціалізація…</string>

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">用户名</string> <string name="entry_user_name">用户名</string>
<string name="error_arc4">不支持Arcfour流式加密。</string> <string name="error_arc4">不支持Arcfour流式加密。</string>
<string name="error_can_not_handle_uri">无法在KeePassDX中处理此URI。</string> <string name="error_can_not_handle_uri">无法在KeePassDX中处理此URI。</string>
<string name="error_file_not_create">无法新建文件</string> <string name="error_file_not_create">无法新建文件</string>
<string name="error_invalid_db">无法读取数据库。</string> <string name="error_invalid_db">无法读取数据库。</string>
<string name="error_invalid_path">请确保路径正确。</string> <string name="error_invalid_path">请确保路径正确。</string>
<string name="error_no_name">输入名称。</string> <string name="error_no_name">输入名称。</string>
@@ -423,7 +423,7 @@
<string name="database_custom_color_title">自定义数据库颜色</string> <string name="database_custom_color_title">自定义数据库颜色</string>
<string name="compression">压缩</string> <string name="compression">压缩</string>
<string name="compression_none"></string> <string name="compression_none"></string>
<string name="compression_gzip">GZip压缩</string> <string name="compression_gzip">Gzip压缩</string>
<string name="device_keyboard_setting_title">设备键盘设置</string> <string name="device_keyboard_setting_title">设备键盘设置</string>
<string name="error_save_database">无法保存数据库。</string> <string name="error_save_database">无法保存数据库。</string>
<string name="menu_save_database">保存数据库</string> <string name="menu_save_database">保存数据库</string>
@@ -444,7 +444,7 @@
<string name="download_initialization">正在初始化…</string> <string name="download_initialization">正在初始化…</string>
<string name="download_progression">进行中:%1$d%%</string> <string name="download_progression">进行中:%1$d%%</string>
<string name="download_finalization">正在完成…</string> <string name="download_finalization">正在完成…</string>
<string name="download_complete">完成!点击打开文件。</string> <string name="download_complete">完成!</string>
<string name="hide_expired_entries_title">隐藏过期条目</string> <string name="hide_expired_entries_title">隐藏过期条目</string>
<string name="hide_expired_entries_summary">过期条目将被隐藏</string> <string name="hide_expired_entries_summary">过期条目将被隐藏</string>
<string name="contact">联系我们</string> <string name="contact">联系我们</string>

View File

@@ -56,7 +56,7 @@
<string name="entry_user_name">用戶名</string> <string name="entry_user_name">用戶名</string>
<string name="error_arc4">Arcfour流密碼不被支援。</string> <string name="error_arc4">Arcfour流密碼不被支援。</string>
<string name="error_can_not_handle_uri">KeePassDX無法處理此URI。</string> <string name="error_can_not_handle_uri">KeePassDX無法處理此URI。</string>
<string name="error_file_not_create">不能創建檔案</string> <string name="error_file_not_create">不能創建檔案</string>
<string name="error_invalid_db">無法閱讀資料庫。</string> <string name="error_invalid_db">無法閱讀資料庫。</string>
<string name="error_invalid_path">請確保路徑正確。</string> <string name="error_invalid_path">請確保路徑正確。</string>
<string name="error_no_name">請輸入用戶名。</string> <string name="error_no_name">請輸入用戶名。</string>

View File

@@ -103,7 +103,7 @@
<string name="entry_user_name">Username</string> <string name="entry_user_name">Username</string>
<string name="error_arc4">The Arcfour stream cipher is not supported.</string> <string name="error_arc4">The Arcfour stream cipher is not supported.</string>
<string name="error_can_not_handle_uri">Could not handle this URI in KeePassDX.</string> <string name="error_can_not_handle_uri">Could not handle this URI in KeePassDX.</string>
<string name="error_file_not_create">Could not create file:</string> <string name="error_file_not_create">Could not create file</string>
<string name="error_invalid_db">Could not read the database.</string> <string name="error_invalid_db">Could not read the database.</string>
<string name="error_invalid_path">Make sure the path is correct.</string> <string name="error_invalid_path">Make sure the path is correct.</string>
<string name="error_invalid_OTP">Invalid OTP secret.</string> <string name="error_invalid_OTP">Invalid OTP secret.</string>
@@ -257,6 +257,9 @@
<string name="warning_empty_password">Continue without password unlocking protection?</string> <string name="warning_empty_password">Continue without password unlocking protection?</string>
<string name="warning_no_encryption_key">Continue without encryption key?</string> <string name="warning_no_encryption_key">Continue without encryption key?</string>
<string name="warning_permanently_delete_nodes">Permanently delete selected nodes?</string> <string name="warning_permanently_delete_nodes">Permanently delete selected nodes?</string>
<string name="warning_file_too_big">A KeePass database is supposed to contain only small utility files (such as PGP key files).\n\nYour database may get very large and reduce performance with this upload.</string>
<string name="warning_replace_file">Uploading this file will replace the existing one.</string>
<string name="warning_sure_add_file">Add the file anyway?</string>
<string name="version_label">Version %1$s</string> <string name="version_label">Version %1$s</string>
<string name="build_label">Build %1$s</string> <string name="build_label">Build %1$s</string>
<string name="configure_biometric">Biometric prompt is supported, but not set up.</string> <string name="configure_biometric">Biometric prompt is supported, but not set up.</string>
@@ -351,7 +354,7 @@
<string name="other">Other</string> <string name="other">Other</string>
<string name="compression">Compression</string> <string name="compression">Compression</string>
<string name="compression_none">None</string> <string name="compression_none">None</string>
<string name="compression_gzip">gzip</string> <string name="compression_gzip">Gzip</string>
<string name="recycle_bin">Recycle bin</string> <string name="recycle_bin">Recycle bin</string>
<string name="keyboard">Keyboard</string> <string name="keyboard">Keyboard</string>
<string name="magic_keyboard_title">Magikeyboard</string> <string name="magic_keyboard_title">Magikeyboard</string>
@@ -451,10 +454,11 @@
<string name="download">Download</string> <string name="download">Download</string>
<string name="contribute">Contribute</string> <string name="contribute">Contribute</string>
<string name="download_attachment">Download %1$s</string> <string name="download_attachment">Download %1$s</string>
<string name="upload_attachment">Upload %1$s</string>
<string name="download_initialization">Initializing…</string> <string name="download_initialization">Initializing…</string>
<string name="download_progression">In progress: %1$d%%</string> <string name="download_progression">In progress: %1$d%%</string>
<string name="download_finalization">Finalizing…</string> <string name="download_finalization">Finalizing…</string>
<string name="download_complete">Tap to open the file.</string> <string name="download_complete">Complete!</string>
<string name="encryption_rijndael">Rijndael (AES)</string> <string name="encryption_rijndael">Rijndael (AES)</string>
<string name="encryption_twofish">Twofish</string> <string name="encryption_twofish">Twofish</string>
<string name="encryption_chacha20">ChaCha20</string> <string name="encryption_chacha20">ChaCha20</string>

View File

@@ -1 +1 @@
* * Add attachments

View File

@@ -1 +1 @@
* * Ajout des fichiers joints