Merge branch 'release/2.8.3'

This commit is contained in:
J-Jamet
2020-09-02 12:29:36 +02:00
122 changed files with 3028 additions and 1562 deletions

View File

@@ -1,3 +1,10 @@
KeePassDX(2.8.3)
* Upload attachments
* Visibility button for each hidden field
* Fix read header file
* Fix deletion in KDB database
* Fix minor issues
KeePassDX(2.8.2)
* Fix themes / new UI
* Fix multiples notifications

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass"
minSdkVersion 14
targetSdkVersion 29
versionCode = 38
versionName = "2.8.2"
versionCode = 39
versionName = "2.8.3"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -45,8 +45,9 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.magikeyboard.MagikIME
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
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 mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAttachmentsToDownload: HashMap<Int, EntryAttachment> = HashMap()
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
private var clipboardHelper: ClipboardHelper? = null
private var mFirstLaunchOfActivity: Boolean = false
@@ -212,8 +213,8 @@ class EntryActivity : LockingActivity() {
mAttachmentFileBinderManager?.apply {
registerProgressTask()
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
entryContentsView?.updateAttachmentDownloadProgress(attachment)
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
entryContentsView?.putAttachment(entryAttachmentState)
}
}
}
@@ -240,14 +241,13 @@ class EntryActivity : LockingActivity() {
toolbar?.title = entryTitle
// Assign basic fields
entryContentsView?.assignUserName(entry.username)
entryContentsView?.assignUserNameCopyListener(View.OnClickListener {
entryContentsView?.assignUserName(entry.username) {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.username,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
database.stopManageEntry(entry)
})
}
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
@@ -274,23 +274,25 @@ class EntryActivity : LockingActivity() {
}
}
entryContentsView?.assignPassword(entry.password, allowCopyPasswordAndProtectedFields)
if (allowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(View.OnClickListener {
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
View.OnClickListener {
database.startManageEntry(entry)
clipboardHelper?.timeoutCopyToClipboard(entry.password,
getString(R.string.copy_field,
getString(R.string.copy_field,
getString(R.string.entry_password)))
database.stopManageEntry(entry)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
entryContentsView?.assignPasswordCopyListener(showWarningClipboardDialogOnClickListener)
showWarningClipboardDialogOnClickListener
} else {
entryContentsView?.assignPasswordCopyListener(null)
null
}
}
entryContentsView?.assignPassword(entry.password,
allowCopyPasswordAndProtectedFields,
onPasswordCopyClickListener)
//Assign OTP field
entryContentsView?.assignOtp(entry.getOtpElement(), entryProgress,
@@ -304,7 +306,7 @@ class EntryActivity : LockingActivity() {
})
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignComment(entry.notes)
entryContentsView?.assignNotes(entry.notes)
// Assign custom fields
if (entry.allowCustomFields()) {
@@ -312,12 +314,12 @@ class EntryActivity : LockingActivity() {
for ((label, value) in entry.customFields) {
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
if (allowCopyProtectedField) {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, View.OnClickListener {
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
clipboardHelper?.timeoutCopyToClipboard(
value.toString(),
getString(R.string.copy_field, label)
)
})
}
} else {
// If dialog not already shown
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
@@ -328,18 +330,13 @@ class EntryActivity : LockingActivity() {
}
}
}
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
// Manage attachments
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem ->
when (attachmentItem.downloadState) {
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
else -> {
// TODO Stop download
mDatabase?.binaryPool?.let { binaryPool ->
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool).toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
createDocument(this, attachmentItem.name)?.let { requestCode ->
mAttachmentsToDownload[requestCode] = attachmentItem
}
}
}
@@ -393,16 +390,6 @@ class EntryActivity : LockingActivity() {
}
}
private fun changeShowPasswordIcon(togglePassword: MenuItem?) {
if (mShowPassword) {
togglePassword?.setTitle(R.string.menu_hide_password)
togglePassword?.setIcon(R.drawable.ic_visibility_off_white_24dp)
} else {
togglePassword?.setTitle(R.string.menu_showpass)
togglePassword?.setIcon(R.drawable.ic_visibility_white_24dp)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
@@ -418,15 +405,6 @@ class EntryActivity : LockingActivity() {
menu.findItem(R.id.menu_edit)?.isVisible = false
}
val togglePassword = menu.findItem(R.id.menu_toggle_pass)
entryContentsView?.let {
if (it.isPasswordPresent || it.atLeastOneFieldProtectedPresent()) {
changeShowPasswordIcon(togglePassword)
} else {
togglePassword?.isVisible = false
}
}
val gotoUrl = menu.findItem(R.id.menu_goto_url)
gotoUrl?.apply {
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
@@ -449,28 +427,31 @@ class EntryActivity : LockingActivity() {
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
menu: Menu) {
val entryCopyEducationPerformed = entryContentsView?.isUserNamePresent == true
val entryFieldCopyView = findViewById<View>(R.id.entry_field_copy)
val entryCopyEducationPerformed = entryFieldCopyView != null
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
findViewById(R.id.entry_user_name_action_image),
entryFieldCopyView,
{
clipboardHelper?.timeoutCopyToClipboard(mEntry!!.username,
getString(R.string.copy_field,
getString(R.string.entry_user_name)))
val appNameString = getString(R.string.app_name)
clipboardHelper?.timeoutCopyToClipboard(appNameString,
getString(R.string.copy_field, appNameString))
},
{
performedNextEducation(entryActivityEducation, menu)
})
if (!entryCopyEducationPerformed) {
val menuEditView = toolbar?.findViewById<View>(R.id.menu_edit)
// entryEditEducationPerformed
toolbar?.findViewById<View>(R.id.menu_edit) != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
toolbar!!.findViewById(R.id.menu_edit),
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
performedNextEducation(entryActivityEducation, menu)
})
menuEditView != null && entryActivityEducation.checkAndPerformedEntryEditEducation(
menuEditView,
{
onOptionsItemSelected(menu.findItem(R.id.menu_edit))
},
{
performedNextEducation(entryActivityEducation, menu)
}
)
}
}
@@ -480,12 +461,6 @@ class EntryActivity : LockingActivity() {
MenuUtil.onContributionItemSelected(this)
return true
}
R.id.menu_toggle_pass -> {
mShowPassword = !mShowPassword
changeShowPasswordIcon(item)
entryContentsView?.setHiddenPasswordStyle(!mShowPassword)
return true
}
R.id.menu_edit -> {
mEntry?.let {
EntryEditActivity.launch(this@EntryActivity, it)

View File

@@ -22,6 +22,8 @@ import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -36,18 +38,17 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import com.kunzisoft.keepass.R
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.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.education.EntryEditActivityEducation
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService
import com.kunzisoft.keepass.notifications.ClipboardEntryNotificationService
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
import com.kunzisoft.keepass.notifications.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
@@ -55,8 +56,10 @@ import com.kunzisoft.keepass.notifications.KeyboardEntryNotificationService
import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.EntryEditContentsView
import com.kunzisoft.keepass.view.showActionError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
@@ -69,7 +72,9 @@ class EntryEditActivity : LockingActivity(),
GeneratePasswordDialogFragment.GeneratePasswordListener,
SetOTPDialogFragment.CreateOtpListener,
DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener {
TimePickerDialog.OnTimeSetListener,
FileTooBigDialogFragment.ActionChooseListener,
ReplaceFileDialogFragment.ActionChooseListener {
private var mDatabase: Database? = null
@@ -90,6 +95,11 @@ class EntryEditActivity : LockingActivity(),
private var mFocusedEditExtraField: FocusedEditField? = null
// To manage attachments
private var mSelectFileHelper: SelectFileHelper? = null
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
private var mAllowMultipleAttachments: Boolean = false
// Education
private var entryEditActivityEducation: EntryEditActivityEducation? = null
@@ -215,22 +225,22 @@ class EntryEditActivity : LockingActivity(),
entryEditAddToolBar?.apply {
menuInflater.inflate(R.menu.entry_edit, menu)
menu.findItem(R.id.menu_add_field).apply {
val allowLock = PreferencesUtil.showLockDatabaseButton(context)
isEnabled = allowLock
isVisible = allowLock
}
menu.findItem(R.id.menu_add_field).apply {
val allowCustomField = mNewEntry?.allowCustomFields() == true
isEnabled = 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 {
val allowOTP = mDatabase?.allowOTP == true
isEnabled = allowOTP
isVisible = allowOTP
// OTP not compatible below KitKat
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
}
setOnMenuItemClickListener { item ->
@@ -239,6 +249,10 @@ class EntryEditActivity : LockingActivity(),
addNewCustomField()
true
}
R.id.menu_add_attachment -> {
addNewAttachment(item)
true
}
R.id.menu_add_otp -> {
setupOTP()
true
@@ -248,6 +262,10 @@ class EntryEditActivity : LockingActivity(),
}
}
// To retrieve attachment
mSelectFileHelper = SelectFileHelper(this)
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
// Save button
validateButton = findViewById(R.id.entry_edit_validate)
validateButton?.setOnClickListener { saveEntry() }
@@ -279,6 +297,54 @@ class EntryEditActivity : LockingActivity(),
// Padding if lock button visible
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 -> {
entryEditContentsView?.apply {
// When only one attachment is allowed
if (!mAllowMultipleAttachments) {
clearAttachments()
}
putAttachment(entryAttachmentState)
requestLayout()
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.IN_PROGRESS -> {
entryEditContentsView?.putAttachment(entryAttachmentState)
}
AttachmentState.COMPLETE -> {
entryEditContentsView?.apply {
putAttachment(entryAttachmentState)
// Scroll to the attachment position
getAttachmentViewPosition(entryAttachmentState) {
scrollView?.smoothScrollTo(0, it.toInt())
}
}
}
AttachmentState.ERROR -> {
mDatabase?.removeAttachmentIfNotUsed(entryAttachmentState.attachment)
entryEditContentsView?.removeAttachment(entryAttachmentState)
}
else -> {}
}
}
}
}
}
override fun onPause() {
mAttachmentFileBinderManager?.unregisterProgressTask()
super.onPause()
}
private fun populateViewsWithEntry(newEntry: Entry) {
@@ -303,9 +369,14 @@ class EntryEditActivity : LockingActivity(),
notes = newEntry.notes
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
Field(it.key, it.value)
}, {
editCustomField(it)
}, mFocusedEditExtraField)
assignAttachments(newEntry.getAttachments()) { attachment ->
newEntry.removeAttachment(attachment)
mDatabase?.binaryPool?.let { binaryPool ->
assignAttachments(newEntry.getAttachments(binaryPool).toSet(), StreamDirection.UPLOAD) { attachment ->
newEntry.removeAttachment(attachment)
}
}
}
}
@@ -327,9 +398,14 @@ class EntryEditActivity : LockingActivity(),
expiryTime = entryView.expiresDate
}
notes = entryView.notes
entryView.getExtraField().forEach { customField ->
entryView.getExtraFields().forEach { customField ->
putExtraField(customField.name, customField.protectedValue)
}
mDatabase?.binaryPool?.let { binaryPool ->
entryView.getAttachments().forEach {
putAttachment(it, binaryPool)
}
}
mFocusedEditExtraField = entryView.getExtraFieldFocused()
}
}
@@ -358,12 +434,84 @@ class EntryEditActivity : LockingActivity(),
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
}
override fun onNewCustomFieldApproved(label: String, protection: Boolean) {
entryEditContentsView?.putExtraField(Field(label, ProtectedString(protection)))
private fun editCustomField(field: Field) {
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
}
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
override fun onNewCustomFieldApproved(newField: Field) {
entryEditContentsView?.apply {
putExtraField(newField)
getExtraFieldViewPosition(newField) { position ->
scrollView?.smoothScrollTo(0, position.toInt())
}
}
}
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
entryEditContentsView?.replaceExtraField(oldField, newField)
}
override fun onDeleteCustomFieldApproved(oldField: Field) {
entryEditContentsView?.removeExtraField(oldField)
}
/**
* 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() {
// Retrieve the current otpElement if exists
// and open the dialog to set up the OTP
@@ -441,8 +589,8 @@ class EntryEditActivity : LockingActivity(),
if (!generatePasswordEducationPerformed) {
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
val addNewFieldEducationPerformed = mNewEntry != null
&& mNewEntry!!.allowCustomFields() && mNewEntry!!.customFields.isEmpty()
&& addNewFieldView != null && addNewFieldView.visibility == View.VISIBLE
&& mNewEntry!!.allowCustomFields() && addNewFieldView != null
&& addNewFieldView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
addNewFieldView,
{
@@ -453,13 +601,27 @@ class EntryEditActivity : LockingActivity(),
}
)
if (!addNewFieldEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
val addAttachmentEducationPerformed = attachmentView != null && attachmentView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
attachmentView,
{
setupOTP()
})
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(attachmentView)
},
{
performedNextEducation(entryEditActivityEducation)
}
)
if (!addAttachmentEducationPerformed) {
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
setupOtpView != null && setupOtpView.visibility == View.VISIBLE
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
setupOtpView,
{
setupOTP()
}
)
}
}
}
}
@@ -485,8 +647,13 @@ class EntryEditActivity : LockingActivity(),
// Update the otp field with otpauth:// url
val otpField = OtpEntryFields.buildOtpField(otpElement,
mEntry?.title, mEntry?.username)
entryEditContentsView?.putExtraField(otpField)
mEntry?.putExtraField(otpField.name, otpField.protectedValue)
entryEditContentsView?.apply {
putExtraField(otpField)
getExtraFieldViewPosition(otpField) { position ->
scrollView?.smoothScrollTo(0, position.toInt())
}
}
}
override fun iconPicked(bundle: Bundle) {

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
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.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -69,7 +69,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
// Views
private var coordinatorLayout: CoordinatorLayout? = null
private var fileManagerExplanationButton: View? = null
private var createDatabaseButtonView: View? = null
private var openDatabaseButtonView: View? = null
@@ -82,7 +81,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
private var mDatabaseFileUri: Uri? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
@@ -98,20 +97,15 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
toolbar.title = ""
setSupportActionBar(toolbar)
fileManagerExplanationButton = findViewById(R.id.file_manager_explanation_button)
fileManagerExplanationButton?.setOnClickListener {
UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
// Create database button
createDatabaseButtonView = findViewById(R.id.create_database_button)
createDatabaseButtonView?.setOnClickListener { createNewFile() }
// Open database button
mOpenFileHelper = OpenFileHelper(this)
mSelectFileHelper = SelectFileHelper(this)
openDatabaseButtonView = findViewById(R.id.open_keyfile_button)
openDatabaseButtonView?.apply {
mOpenFileHelper?.openFileOnClickViewListener?.let {
mSelectFileHelper?.selectFileOnClickViewListener?.let {
setOnClickListener(it)
setOnLongClickListener(it)
}
@@ -389,7 +383,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
AutofillHelper.onActivityResultSetResultAndFinish(this, requestCode, resultCode, data)
}
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
if (uri != null) {
launchPasswordActivityWithPath(uri)
}
@@ -445,7 +439,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
openDatabaseButtonView!!,
{tapTargetView ->
tapTargetView?.let {
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
}
},
{}
@@ -454,6 +448,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> UriUtil.gotoUrl(this, R.string.file_manager_explanation_url)
}
return MenuUtil.onDefaultMenuOptionsItemSelected(this, item) && super.onOptionsItemSelected(item)
}

View File

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

View File

@@ -25,16 +25,16 @@ import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
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.TextWatcher
import android.view.View
import android.widget.CompoundButton
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.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.view.KeyFileSelectionView
class AssignMasterKeyDialogFragment : DialogFragment() {
@@ -56,7 +56,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
private var mListener: AssignPasswordDialogListener? = null
private var mOpenFileHelper: OpenFileHelper? = null
private var mSelectFileHelper: SelectFileHelper? = null
private val passwordTextWatcher = object : TextWatcher {
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)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
mOpenFileHelper = OpenFileHelper(this)
mSelectFileHelper = SelectFileHelper(this)
keyFileSelectionView?.apply {
setOnClickListener(mOpenFileHelper?.openFileOnClickViewListener)
setOnLongClickListener(mOpenFileHelper?.openFileOnClickViewListener)
setOnClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
setOnLongClickListener(mSelectFileHelper?.selectFileOnClickViewListener)
}
val dialog = builder.create()
@@ -249,8 +249,7 @@ class AssignMasterKeyDialogFragment : DialogFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mOpenFileHelper?.onActivityResultCallback(requestCode, resultCode, data
) { uri ->
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
uri?.let { pathUri ->
keyFileCheckBox?.isChecked = true
keyFileSelectionView?.uri = pathUri

View File

@@ -22,24 +22,31 @@ package com.kunzisoft.keepass.activities.dialogs
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.view.inputmethod.EditorInfo
import android.widget.Button
import android.widget.CompoundButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
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.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.Field
class EntryCustomFieldDialogFragment: DialogFragment() {
private var oldField: Field? = null
private var entryCustomFieldListener: EntryCustomFieldListener? = null
private var customFieldLabelContainer: TextInputLayout? = null
private var customFieldLabel: TextView? = null
private var customFieldDeleteButton: ImageView? = null
private var customFieldProtectionButton: CompoundButton? = null
override fun onAttach(context: Context) {
@@ -58,17 +65,27 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
val root = activity.layoutInflater.inflate(R.layout.fragment_entry_new_field, null)
customFieldLabelContainer = root?.findViewById(R.id.entry_custom_field_label_container)
customFieldLabel = root?.findViewById(R.id.entry_custom_field_label)
customFieldDeleteButton = root?.findViewById(R.id.entry_custom_field_delete)
customFieldProtectionButton = root?.findViewById(R.id.entry_custom_field_protection)
oldField = arguments?.getParcelable(KEY_FIELD)
oldField?.let { oldCustomField ->
customFieldLabel?.text = oldCustomField.name
customFieldProtectionButton?.isChecked = oldCustomField.protectedValue.isProtected
customFieldDeleteButton?.visibility = View.VISIBLE
customFieldDeleteButton?.setOnClickListener {
entryCustomFieldListener?.onDeleteCustomFieldApproved(oldCustomField)
(dialog as AlertDialog?)?.dismiss()
}
} ?: run {
customFieldDeleteButton?.visibility = View.GONE
}
val builder = AlertDialog.Builder(activity)
builder.setView(root)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(android.R.string.cancel) { _, _ ->
entryCustomFieldListener?.onNewCustomFieldCanceled(
customFieldLabel?.text.toString(),
customFieldProtectionButton?.isChecked == true
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
val dialogCreated = builder.create()
customFieldLabel?.requestFocus()
@@ -102,10 +119,19 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
private fun approveIfValid() {
if (isValid()) {
entryCustomFieldListener?.onNewCustomFieldApproved(
customFieldLabel?.text.toString(),
customFieldProtectionButton?.isChecked == true
)
oldField?.let {
// New property with old value
entryCustomFieldListener?.onEditCustomFieldApproved(it,
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true,
it.protectedValue.stringValue))
)
} ?: run {
entryCustomFieldListener?.onNewCustomFieldApproved(
Field(customFieldLabel?.text?.toString() ?: "",
ProtectedString(customFieldProtectionButton?.isChecked == true))
)
}
(dialog as AlertDialog?)?.dismiss()
}
}
@@ -127,13 +153,25 @@ class EntryCustomFieldDialogFragment: DialogFragment() {
}
interface EntryCustomFieldListener {
fun onNewCustomFieldApproved(label: String, protection: Boolean)
fun onNewCustomFieldCanceled(label: String, protection: Boolean)
fun onNewCustomFieldApproved(newField: Field)
fun onEditCustomFieldApproved(oldField: Field, newField: Field)
fun onDeleteCustomFieldApproved(oldField: Field)
}
companion object {
private const val KEY_FIELD = "KEY_FIELD"
fun getInstance(): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment()
}
fun getInstance(field: Field): EntryCustomFieldDialogFragment {
return EntryCustomFieldDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(KEY_FIELD, field)
}
}
}
}
}

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.os.Build
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.kunzisoft.keepass.activities.dialogs.FileManagerDialogFragment
import com.kunzisoft.keepass.utils.UriUtil
class OpenFileHelper {
class SelectFileHelper {
private var activity: Activity? = null
private var fragment: Fragment? = null
val openFileOnClickViewListener: OpenFileOnClickViewListener
get() = OpenFileOnClickViewListener()
val selectFileOnClickViewListener: SelectFileOnClickViewListener
get() = SelectFileOnClickViewListener()
constructor(context: Activity) {
this.activity = context
@@ -52,7 +53,10 @@ class OpenFileHelper {
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) {
try {
@@ -85,6 +89,11 @@ class OpenFileHelper {
onAbstractClick(true)
return true
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
onAbstractClick()
return true
}
}
@SuppressLint("InlinedApi")

View File

@@ -32,6 +32,18 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
open fun isEmpty(): Boolean {
return itemsList.isEmpty()
}
open fun contains(item: Item): Boolean {
return itemsList.contains(item)
}
open fun indexOf(item: Item): Int {
return itemsList.indexOf(item)
}
open fun putItem(item: Item) {
val previousSize = itemsList.size
if (itemsList.contains(item)) {
@@ -46,13 +58,42 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
}
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
/**
* Only replace [oldItem] by [newItem] if [oldItem] exists
*/
open fun replaceItem(oldItem: Item, newItem: Item) {
if (itemsList.contains(oldItem)) {
val index = itemsList.indexOf(oldItem)
itemsList.removeAt(index)
itemsList.add(index, newItem)
notifyItemChanged(index)
}
}
/**
* Only remove [item] if doesn't exists
*/
open fun removeItem(item: Item) {
if (itemsList.contains(item)) {
mItemToRemove = item
notifyItemChanged(itemsList.indexOf(item))
}
}
protected fun performDeletion(holder: T, item: Item): Boolean {
val effectivelyDeletionPerformed = mItemToRemove == item
if (effectivelyDeletionPerformed) {
holder.itemView.collapse(true) {
deleteItem(item)
}
}
return effectivelyDeletionPerformed
}
protected fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
deleteButton.apply {
visibility = View.VISIBLE
if (mItemToRemove == item) {
holder.itemView.collapse(true) {
deleteItem(item)
}
if (performDeletion(holder, item)) {
setOnClickListener(null)
} else {
setOnClickListener {

View File

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

View File

@@ -45,6 +45,8 @@ class EntryExtraFieldsItemsAdapter(context: Context)
private var mLastFocusedEditField = FocusedEditField()
private var mLastFocusedTimestamp: Long = 0L
var onEditButtonClickListener: ((item: Field)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryExtraFieldViewHolder {
val view = EntryExtraFieldViewHolder(
inflater.inflate(R.layout.item_entry_edit_extra_field, parent, false)
@@ -73,11 +75,10 @@ class EntryExtraFieldsItemsAdapter(context: Context)
setFocusField(extraField, selectionStart, selectionEnd)
} else {
// request focus on last text focused
if (focusedTimestampNotExpired()) {
if (focusedTimestampNotExpired())
requestFocusField(this, extraField, false)
} else {
else
removeFocusField(extraField)
}
}
}
addOnSelectionChangedListener(object: EditTextSelectable.OnSelectionChangedListener {
@@ -95,9 +96,10 @@ class EntryExtraFieldsItemsAdapter(context: Context)
if (applyFontVisibility)
applyFontVisibility()
}
holder.extraFieldDeleteButton.apply {
onBindDeleteButton(holder, this, extraField, position)
holder.extraFieldEditButton.setOnClickListener {
onEditButtonClickListener?.invoke(extraField)
}
performDeletion(holder, extraField)
}
fun assignItems(items: List<Field>, focusedEditField: FocusedEditField?) {
@@ -107,15 +109,6 @@ class EntryExtraFieldsItemsAdapter(context: Context)
super.assignItems(items)
}
override fun putItem(item: Field) {
setFocusField(mLastFocusedEditField.apply {
field = item
cursorSelectionStart = -1
cursorSelectionEnd = -1
}, true)
super.putItem(item)
}
private fun setFocusField(field: Field,
selectionStart: Int,
selectionEnd: Int,
@@ -147,6 +140,7 @@ class EntryExtraFieldsItemsAdapter(context: Context)
setEditTextSelection(editText)
}
requestFocus()
removeFocusField(field)
}
}
}
@@ -176,7 +170,7 @@ class EntryExtraFieldsItemsAdapter(context: Context)
class EntryExtraFieldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var extraFieldValueContainer: TextInputLayout = itemView.findViewById(R.id.entry_extra_field_value_container)
var extraFieldValue: EditTextSelectable = itemView.findViewById(R.id.entry_extra_field_value)
var extraFieldDeleteButton: View = itemView.findViewById(R.id.entry_extra_field_delete)
var extraFieldEditButton: View = itemView.findViewById(R.id.entry_extra_field_edit)
}
companion object {

View File

@@ -101,9 +101,9 @@ class FileDatabaseHistoryAdapter(context: Context)
// Modification
databaseFile.databaseLastModified?.let {
holder.fileModification.text = it
holder.fileModification.visibility = View.VISIBLE
holder.fileModificationContainer.visibility = View.VISIBLE
} ?: run {
holder.fileModification.visibility = View.GONE
holder.fileModificationContainer.visibility = View.GONE
}
// Size
@@ -234,7 +234,7 @@ class FileDatabaseHistoryAdapter(context: Context)
this.saveAliasListener = listener
}
inner class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info)
@@ -250,6 +250,7 @@ class FileDatabaseHistoryAdapter(context: Context)
var fileModifyButton: ImageView = itemView.findViewById(R.id.file_modify_button)
var fileDeleteButton: ImageView = itemView.findViewById(R.id.file_delete_button)
var filePath: TextView = itemView.findViewById(R.id.file_path)
var fileModificationContainer: ViewGroup = itemView.findViewById(R.id.file_modification_container)
var fileModification: TextView = itemView.findViewById(R.id.file_modification)
var fileSize: TextView = itemView.findViewById(R.id.file_size)
}

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)
}
@@ -391,6 +394,7 @@ class NodeAdapter (private val context: Context)
var text: TextView = itemView.findViewById(R.id.node_text)
var subText: TextView = itemView.findViewById(R.id.node_subtext)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
}
companion object {

View File

@@ -119,15 +119,19 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
IOActionTask(
{
databaseFileToAddOrUpdate.databaseUri?.let { databaseUri ->
// Try to get info in database first
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(databaseUri.toString())
// Complete alias if not exists
val fileDatabaseHistory = FileDatabaseHistoryEntity(
databaseUri.toString(),
databaseFileToAddOrUpdate.databaseAlias ?: "",
databaseFileToAddOrUpdate.databaseAlias
?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(),
System.currentTimeMillis()
)
val fileDatabaseHistoryRetrieve = databaseFileHistoryDao.getByDatabaseUri(fileDatabaseHistory.databaseUri)
// Update values if history element not yet in the database
if (fileDatabaseHistoryRetrieve == null) {
databaseFileHistoryDao.add(fileDatabaseHistory)

View File

@@ -34,7 +34,7 @@ class DeleteEntryHistoryDatabaseRunnable (
override fun onStartRun() {
try {
mainEntry.removeEntryFromHistory(entryHistoryPosition)
database.removeEntryHistory(mainEntry, entryHistoryPosition)
} catch (e: Exception) {
setError(e)
}

View File

@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
} else {
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
import android.content.Context
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
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
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool, true)
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool, true)
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
mOldEntry.updateWith(mNewEntry)
mNewEntry.touch(modified = true, touchParents = true)
// Create an entry history (an entry history don't have history)
mOldEntry.addEntryToHistory(Entry(mBackupEntryHistory, copyHistory = false))
database.removeOldestEntryHistory(mOldEntry)
database.removeOldestEntryHistory(mOldEntry, database.binaryPool)
// Only change data in index
database.updateEntry(mOldEntry)
// Remove oldest attachments
attachmentsToRemove.forEach {
database.removeAttachmentIfNotUsed(it)
}
}
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,9 +25,7 @@ import android.net.Uri
import android.util.Log
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDB
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.*
import com.kunzisoft.keepass.database.element.icon.IconImageFactory
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdInt
@@ -52,6 +50,7 @@ import com.kunzisoft.keepass.utils.SingletonHolder
import com.kunzisoft.keepass.utils.UriUtil
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
class Database {
@@ -157,6 +156,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,
newCompression: CompressionAlgorithm) {
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
@@ -268,14 +278,14 @@ class Database {
}
/**
* Determine if RecycleBin is available or not for this version of database
* @return true if RecycleBin available
* Determine if a configurable RecycleBin is available or not for this version of database
* @return true if a configurable RecycleBin available
*/
val allowRecycleBin: Boolean
val allowConfigurableRecycleBin: Boolean
get() = mDatabaseKDBX != null
var isRecycleBinEnabled: Boolean
// TODO #394 isRecycleBinEnabled mDatabaseKDB
// Backup is always enabled in KDB database
get() = mDatabaseKDB != null || mDatabaseKDBX?.isRecycleBinEnabled ?: false
set(value) {
mDatabaseKDBX?.isRecycleBinEnabled = value
@@ -293,12 +303,12 @@ class Database {
}
fun ensureRecycleBinExists(resources: Resources) {
mDatabaseKDB?.ensureRecycleBinExists()
mDatabaseKDB?.ensureBackupExists()
mDatabaseKDBX?.ensureRecycleBinExists(resources)
}
fun removeRecycleBin() {
// TODO #394 delete backup mDatabaseKDB?.removeRecycleBin()
// Don't allow remove backup in KDB
mDatabaseKDBX?.removeRecycleBin()
}
@@ -428,6 +438,37 @@ class Database {
}, 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)
fun saveData(contentResolver: ContentResolver) {
try {
@@ -473,7 +514,7 @@ class Database {
} else {
var outputStream: OutputStream? = null
try {
outputStream = contentResolver.openOutputStream(uri)
outputStream = contentResolver.openOutputStream(uri, "rwt")
outputStream?.let { definedOutputStream ->
val databaseOutput = mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let { DatabaseOutputKDBX(it, definedOutputStream) }
@@ -718,7 +759,7 @@ class Database {
fun canRecycle(entry: Entry): Boolean {
var canRecycle: Boolean? = null
entry.entryKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle()
canRecycle = mDatabaseKDB?.canRecycle(it)
}
entry.entryKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -729,7 +770,7 @@ class Database {
fun canRecycle(group: Group): Boolean {
var canRecycle: Boolean? = null
group.groupKDB?.let {
canRecycle = mDatabaseKDB?.canRecycle()
canRecycle = mDatabaseKDB?.canRecycle(it)
}
group.groupKDBX?.let {
canRecycle = mDatabaseKDBX?.canRecycle(it)
@@ -800,7 +841,7 @@ class Database {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
removeOldestEntryHistory(node)
removeOldestEntryHistory(node, binaryPool)
return true
}
},
@@ -808,34 +849,19 @@ class Database {
override fun operate(node: Group): Boolean {
return true
}
})
}
fun removeEachEntryHistory() {
rootGroup?.doForEachChildAndForIt(
object : NodeHandler<Entry>() {
override fun operate(node: Entry): Boolean {
node.removeAllHistory()
return true
}
},
object : NodeHandler<Group>() {
override fun operate(node: Group): Boolean {
return true
}
})
}
)
}
/**
* Remove oldest history if more than max items or max memory
*/
fun removeOldestEntryHistory(entry: Entry) {
fun removeOldestEntryHistory(entry: Entry, binaryPool: BinaryPool) {
mDatabaseKDBX?.let {
val maxItems = historyMaxItems
if (maxItems >= 0) {
while (entry.getHistory().size > maxItems) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
}
}
@@ -844,11 +870,10 @@ class Database {
while (true) {
var historySize: Long = 0
for (entryHistory in entry.getHistory()) {
historySize += entryHistory.getSize()
historySize += entryHistory.getSize(binaryPool)
}
if (historySize > maxSize) {
entry.removeOldestEntryFromHistory()
removeOldestEntryHistory(entry)
} else {
break
}
@@ -857,6 +882,22 @@ class Database {
}
}
private fun removeOldestEntryHistory(entry: Entry) {
entry.removeOldestEntryFromHistory()?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
fun removeEntryHistory(entry: Entry, entryHistoryPosition: Int) {
entry.removeEntryFromHistory(entryHistoryPosition)?.let {
it.getAttachments(binaryPool, false).forEach { attachmentToRemove ->
removeAttachmentIfNotUsed(attachmentToRemove)
}
}
}
companion object : SingletonHolder<Database>(::Database) {
private val TAG = Database::class.java.name

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
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.entry.EntryKDB
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.Type
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.Field
import com.kunzisoft.keepass.otp.OtpElement
@@ -317,40 +317,38 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db)
fun startToManageFieldReferences(database: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(database)
}
fun stopToManageFieldReferences() {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
entryKDB?.binaryData?.let { binaryKDB ->
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val attachments = ArrayList<Attachment>()
entryKDB?.getAttachment()?.let {
attachments.add(it)
}
entryKDBX?.binaries?.let { binariesKDBX ->
for ((key, value) in binariesKDBX) {
attachments.add(EntryAttachment(key, value))
}
entryKDBX?.getAttachments(binaryPool, inHistory)?.let {
attachments.addAll(it)
}
return attachments
}
fun removeAttachment(attachment: EntryAttachment) {
entryKDB?.apply {
if (binaryDescription == attachment.name
&& binaryData == attachment.binaryAttachment) {
binaryDescription = ""
binaryData = null
}
}
fun containsAttachment(): Boolean {
return entryKDB?.containsAttachment() == true
|| entryKDBX?.containsAttachment() == true
}
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> {
@@ -368,20 +366,22 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun removeEntryFromHistory(position: Int) {
entryKDBX?.removeEntryFromHistory(position)
fun removeEntryFromHistory(position: Int): Entry? {
entryKDBX?.removeEntryFromHistory(position)?.let {
return Entry(it)
}
return null
}
fun removeAllHistory() {
entryKDBX?.removeAllHistory()
fun removeOldestEntryFromHistory(): Entry? {
entryKDBX?.removeOldestEntryFromHistory()?.let {
return Entry(it)
}
return null
}
fun removeOldestEntryFromHistory() {
entryKDBX?.removeOldestEntryFromHistory()
}
fun getSize(): Long {
return entryKDBX?.size ?: 0L
fun getSize(binaryPool: BinaryPool): Long {
return entryKDBX?.getSize(binaryPool) ?: 0L
}
fun containsCustomData(): Boolean {

View File

@@ -17,10 +17,8 @@
* 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.Parcelable
import com.kunzisoft.keepass.stream.readBytes
@@ -30,7 +28,7 @@ import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable {
var isCompressed: Boolean? = null
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
@@ -46,12 +44,12 @@ class BinaryAttachment : Parcelable {
* Empty protected binary
*/
constructor() {
this.isCompressed = null
this.isCompressed = false
this.isProtected = false
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.isProtected = enableProtection
this.dataFile = dataFile
@@ -59,7 +57,7 @@ class BinaryAttachment : Parcelable {
private constructor(parcel: Parcel) {
val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0
isCompressed = compressedByte != 0
isProtected = parcel.readByte().toInt() != 0
parcel.readString()?.let {
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)
fun compress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
// To compress, create a new binary with file
if (isCompressed != true) {
if (!isCompressed) {
val fileBinaryCompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: GZIPOutputStream? = null
var inputStream: InputStream? = null
try {
outputStream = GZIPOutputStream(FileOutputStream(fileBinaryCompress))
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
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
getInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
// 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)
fun decompress(bufferSize: Int = DEFAULT_BUFFER_SIZE) {
dataFile?.let { concreteDataFile ->
if (isCompressed != false) {
if (isCompressed) {
val fileBinaryDecompress = File(concreteDataFile.parent, concreteDataFile.name + "_temp")
var outputStream: FileOutputStream? = null
var inputStream: GZIPInputStream? = null
try {
outputStream = FileOutputStream(fileBinaryDecompress)
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
FileOutputStream(fileBinaryDecompress).use { outputStream ->
getUnGzipInputDataStream().use { inputStream ->
inputStream.readBytes(bufferSize) { buffer ->
outputStream.write(buffer)
}
}
}
}
}
}
fun download(createdFileUri: Uri,
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) {}
// Remove gzip file
if (concreteDataFile.delete()) {
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
// Harmonize with database compression
isCompressed = false
}
}
}
@@ -185,18 +170,22 @@ class BinaryAttachment : Parcelable {
override fun hashCode(): Int {
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 + dataFile!!.hashCode()
return result
}
override fun toString(): String {
return dataFile.toString()
}
override fun describeContents(): Int {
return 0
}
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.writeString(dataFile?.absolutePath)
}

View File

@@ -19,52 +19,126 @@
*/
package com.kunzisoft.keepass.database.element.database
import android.util.SparseArray
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import java.io.IOException
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? {
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()) {
action.invoke(i, pool.get(pool.keyAt(i)))
/**
* To put a [binaryAttachment] in the pool,
* 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)
fun clear() {
doForEachBinary { _, binary ->
binary.clear()
fun remove(binaryAttachment: BinaryAttachment) {
findKey(binaryAttachment)?.let {
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) {
pool.put(findUnusedKey(), fileBinary)
}
}
fun findUnusedKey(): Int {
var unusedKey = pool.size()
while (get(unusedKey) != null)
/**
* Utility method to find an unused key in the pool
*/
private fun findUnusedKey(): Int {
var unusedKey = 0
while (pool[unusedKey] != null)
unusedKey++
return unusedKey
}
fun findKey(pb: BinaryAttachment): Int? {
for (i in 0 until pool.size()) {
if (pool.get(pool.keyAt(i)) == pb) return i
/**
* Return key of [binaryAttachmentToRetrieve] or null if not found
*/
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

@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB
import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.stream.NullOutputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.DigestOutputStream
@@ -38,7 +40,7 @@ import kotlin.collections.ArrayList
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var backupGroupId: Int = BACKUP_FOLDER_UNDEFINED_ID
private var kdfListV3: MutableList<KdfEngine> = ArrayList()
@@ -57,7 +59,14 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
// Retrieve backup group in index
val backupGroup: GroupKDB?
get() = if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID) null else getGroupById(backupGroupId)
get() {
if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
ensureBackupExists()
return if (backupGroupId == BACKUP_FOLDER_UNDEFINED_ID)
null
else
getGroupById(backupGroupId)
}
override val kdfEngine: KdfEngine?
get() = kdfListV3[0]
@@ -192,10 +201,10 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
}
/**
* Ensure that the recycle bin tree exists, if enabled and create it
* Ensure that the backup tree exists if enabled, and create it
* if it doesn't exist
*/
fun ensureRecycleBinExists() {
fun ensureBackupExists() {
rootGroups.forEach { currentGroup ->
if (currentGroup.level == 0
&& currentGroup.title.equals(BACKUP_FOLDER_TITLE, ignoreCase = true)) {
@@ -219,21 +228,25 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
* @param node Node to remove
* @return true if node can be recycle, false elsewhere
*/
// TODO #394 Backup KDB
// fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
fun canRecycle(): Boolean {
fun canRecycle(node: NodeVersioned<*, GroupKDB, EntryKDB>): Boolean {
if (node == backupGroup)
return false
backupGroup?.let {
if (node.isContainedIn(it))
return false
}
return true
}
fun recycle(group: GroupKDB) {
ensureRecycleBinExists()
ensureBackupExists()
removeGroupFrom(group, group.parent)
addGroupTo(group, backupGroup)
group.afterAssignNewParent()
}
fun recycle(entry: EntryKDB) {
ensureRecycleBinExists()
ensureBackupExists()
removeEntryFrom(entry, entry.parent)
addEntryTo(entry, backupGroup)
entry.afterAssignNewParent()
@@ -249,6 +262,12 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
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 {
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.KdfFactory
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.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.entry.EntryKDBX
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.MemoryProtectionConfig
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_4
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node
import org.w3c.dom.Text
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
@@ -173,33 +177,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
binaryPool.doForEachBinary { key, binary ->
try {
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {
}
CompressionAlgorithm.GZip -> {
// To compress, create a new binary with file
binary.compress(BUFFER_SIZE_BYTES)
}
}
}
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {}
CompressionAlgorithm.GZip -> {
when (newCompression) {
CompressionAlgorithm.None -> {
// To decompress, create a new binary with file
binary.decompress(BUFFER_SIZE_BYTES)
}
CompressionAlgorithm.GZip -> {
}
// Only in databaseV3.1, in databaseV4 the header is zipped during the save
if (kdbxVersion.toKotlinLong() < FILE_VERSION_32_4.toKotlinLong()) {
compressAllBinaries()
}
}
}
}
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) {
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
}
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, true).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 {
if (password == null)
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.NodeKDBInterface
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 kotlin.collections.ArrayList
/**
* Structure containing information about one entry.
@@ -135,6 +137,29 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
override val type: Type
get() = Type.ENTRY
fun getAttachment(): Attachment? {
val binary = binaryData
return if (binary != null)
Attachment(binaryDescription, binary)
else null
}
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 {
/** Size of byte buffer needed to hold this struct. */

View File

@@ -21,7 +21,9 @@ package com.kunzisoft.keepass.database.element.entry
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImage
@@ -31,11 +33,12 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
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.utils.ParcelableUtil
import com.kunzisoft.keepass.utils.UnsignedLong
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.collections.LinkedHashMap
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
@@ -60,8 +63,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
var iconCustom = IconImageCustom.UNKNOWN_ICON
private var customData = LinkedHashMap<String, String>()
// TODO Private
var fields = LinkedHashMap<String, ProtectedString>()
var binaries = LinkedHashMap<String, BinaryAttachment>()
var binaries = LinkedHashMap<String, Int>() // Map<Label, PoolId>
var foregroundColor = ""
var backgroundColor = ""
var overrideURL = ""
@@ -70,36 +74,32 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
var additional = ""
var tags = ""
val size: Long
get() {
var size = FIXED_LENGTH_SIZE
fun getSize(binaryPool: BinaryPool): Long {
var size = FIXED_LENGTH_SIZE
for (entry in fields.entries) {
size += entry.key.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
for (entry in fields.entries) {
size += entry.key.length.toLong()
size += entry.value.length().toLong()
}
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
constructor() : super()
@@ -110,7 +110,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
locationChanged = parcel.readParcelable(DateInstant::class.java.classLoader) ?: locationChanged
customData = ParcelableUtil.readStringParcelableMap(parcel)
fields = ParcelableUtil.readStringParcelableMap(parcel, ProtectedString::class.java)
binaries = ParcelableUtil.readStringParcelableMap(parcel, BinaryAttachment::class.java)
binaries = ParcelableUtil.readStringIntMap(parcel)
foregroundColor = parcel.readString() ?: foregroundColor
backgroundColor = parcel.readString() ?: backgroundColor
overrideURL = parcel.readString() ?: overrideURL
@@ -128,7 +128,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
dest.writeParcelable(locationChanged, flags)
ParcelableUtil.writeStringParcelableMap(dest, customData)
ParcelableUtil.writeStringParcelableMap(dest, flags, fields)
ParcelableUtil.writeStringParcelableMap(dest, flags, binaries)
ParcelableUtil.writeStringIntMap(dest, binaries)
dest.writeString(foregroundColor)
dest.writeString(backgroundColor)
dest.writeString(overrideURL)
@@ -167,8 +167,8 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
tags = source.tags
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
this.mDatabase = db
fun startToManageFieldReferences(database: DatabaseKDBX) {
this.mDatabase = database
this.mDecodeRef = true
}
@@ -284,14 +284,46 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
fields[label] = value
}
fun putProtectedBinary(key: String, value: BinaryAttachment) {
binaries[key] = value
/**
* It's a list because history labels can be defined multiple times
*/
fun getAttachments(binaryPool: BinaryPool, inHistory: Boolean = false): List<Attachment> {
val entryAttachmentList = ArrayList<Attachment>()
for ((label, poolId) in binaries) {
binaryPool[poolId]?.let { binary ->
entryAttachmentList.add(Attachment(label, binary))
}
}
if (inHistory) {
history.forEach {
entryAttachmentList.addAll(it.getAttachments(binaryPool, false))
}
}
return entryAttachmentList
}
fun removeProtectedBinary(name: String) {
binaries.remove(name)
fun containsAttachment(): Boolean {
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 {
return history.size
}
@@ -308,15 +340,11 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
history.add(entry)
}
fun removeEntryFromHistory(position: Int) {
history.removeAt(position)
fun removeEntryFromHistory(position: Int): EntryKDBX? {
return history.removeAt(position)
}
fun removeAllHistory() {
history.clear()
}
fun removeOldestEntryFromHistory() {
fun removeOldestEntryFromHistory(): EntryKDBX? {
var min: Date? = null
var index = -1
@@ -329,9 +357,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
}
}
if (index != -1) {
return if (index != -1) {
history.removeAt(index)
}
} else null
}
override fun touch(modified: Boolean, touchParents: Boolean) {

View File

@@ -192,10 +192,11 @@ class DatabaseHeaderKDBX(private val databaseV4: DatabaseKDBX) : DatabaseHeader(
}
}
if (fieldID == PwDbHeaderV4Fields.EndOfHeader)
return true
if (fieldData != null)
when (fieldID) {
PwDbHeaderV4Fields.EndOfHeader -> return true
PwDbHeaderV4Fields.CipherID -> setCipher(fieldData)
PwDbHeaderV4Fields.CompressionFlags -> setCompressionFlags(fieldData)

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.node.NodeIdInt
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.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeader
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDB
import com.kunzisoft.keepass.stream.*
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import org.joda.time.Instant
import java.io.*
import java.security.*
import java.util.*
@@ -282,11 +280,9 @@ class DatabaseInputKDB(cacheDirectory: File,
0x000E -> {
newEntry?.let { entry ->
if (fieldSize > 0) {
// Generate an unique new file with timestamp
val binaryFile = File(cacheDirectory,
Instant.now().millis.toString())
entry.binaryData = BinaryAttachment(binaryFile)
BufferedOutputStream(FileOutputStream(binaryFile)).use { outputStream ->
val binaryAttachment = mDatabaseToOpen.buildNewBinary(cacheDirectory)
entry.binaryData = binaryAttachment
BufferedOutputStream(binaryAttachment.getOutputDataStream()).use { outputStream ->
cipherInputStream.readBytes(fieldSize,
DatabaseKDB.BUFFER_SIZE_BYTES) { 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.database.element.DateInstant
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.DatabaseKDBX
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.node.NodeIdUUID
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.exception.*
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.XmlPullParserException
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.text.ParseException
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.math.min
@@ -68,9 +71,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var hashOfHeader: ByteArray? = null
private val unusedCacheFileName: String
get() = mDatabase.binaryPool.findUnusedKey().toString()
private var readNextNode = true
private val ctxGroups = Stack<GroupKDBX>()
private var ctxGroup: GroupKDBX? = null
@@ -233,8 +233,10 @@ class DatabaseInputKDBX(cacheDirectory: File,
var data = ByteArray(0)
if (size > 0) {
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary)
if (fieldId != DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary) {
// TODO OOM here
data = dataInputStream.readBytes(size)
}
}
var result = true
@@ -249,18 +251,16 @@ class DatabaseInputKDBX(cacheDirectory: File,
header.innerRandomStreamKey = data
}
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
val file = File(cacheDirectory, unusedCacheFileName)
FileOutputStream(file).use { outputStream ->
val protectedFlag = dataInputStream.readBytes(1)[0].toInt() != 0
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 ->
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)) {
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
if (key != null) {
val pbData = readBinary(xpp)
val id = Integer.parseInt(key)
mDatabase.binaryPool.put(id, pbData!!)
} else {
readUnknown(xpp)
}
readBinary(xpp)
} else {
readUnknown(xpp)
}
@@ -766,8 +759,9 @@ class DatabaseInputKDBX(cacheDirectory: File,
return KdbContext.Entry
} else if (ctx == KdbContext.EntryBinary && name.equals(DatabaseKDBXXML.ElemBinary, ignoreCase = true)) {
if (ctxBinaryName != null && ctxBinaryValue != null)
ctxEntry?.putProtectedBinary(ctxBinaryName!!, ctxBinaryValue!!)
if (ctxBinaryName != null && ctxBinaryValue != null) {
ctxEntry?.putAttachment(Attachment(ctxBinaryName!!, ctxBinaryValue!!), mDatabase.binaryPool)
}
ctxBinaryName = null
ctxBinaryValue = null
@@ -947,50 +941,56 @@ class DatabaseInputKDBX(cacheDirectory: File,
// Reference Id to a binary already present in binary pool
val ref = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrRef)
if (ref != null) {
xpp.next() // Consume end tag
// New id to a binary
val key = xpp.getAttributeValue(null, DatabaseKDBXXML.AttrId)
val id = Integer.parseInt(ref)
return mDatabase.binaryPool[id]
}
// New binary to retrieve
else {
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)
}
return when {
ref != null -> {
xpp.next() // Consume end tag
val id = Integer.parseInt(ref)
// A ref is not necessarily an index in Database V3.1
mDatabase.binaryPool[id]
}
val base64 = readString(xpp)
if (base64.isEmpty())
return BinaryAttachment()
val data = Base64.decode(base64, BASE_64_FLAG)
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)
}
key != null -> {
createBinary(key.toIntOrNull(), xpp)
}
else -> {
// New binary to retrieve
createBinary(null, xpp)
}
}
}
@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)
private fun readString(xpp: XmlPullParser): String {
val buf = readProtectedBase64String(xpp)

View File

@@ -47,18 +47,25 @@ class DatabaseInnerHeaderOutputKDBX(private val database: DatabaseKDBX,
dataOutputStream.writeInt(streamKeySize)
dataOutputStream.write(header.innerRandomStreamKey)
database.binaryPool.doForEachBinary { _, protectedBinary ->
database.binaryPool.doForEachOrderedBinary { _, keyBinary ->
val protectedBinary = keyBinary.binary
var flag = DatabaseHeaderKDBX.KdbxBinaryFlags.None
if (protectedBinary.isProtected) {
flag = flag or DatabaseHeaderKDBX.KdbxBinaryFlags.Protected
}
// Force decompression to add binary in header
protectedBinary.decompress()
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.Binary.toInt())
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1) // TODO verify
dataOutputStream.writeInt(protectedBinary.length().toInt() + 1)
dataOutputStream.write(flag.toInt())
protectedBinary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
dataOutputStream.write(buffer)
// if was compressed in cache, uncompress it
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.icon.IconImageCustom
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.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -55,7 +55,6 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeBinary(binary : BinaryAttachment) {
val binaryLength = binary.length()
if (binaryLength > 0) {
if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(charArray, 0, charArray.size)
}
} else {
// Force binary compression from database (compression was harmonized during import)
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
if (binary.isCompressed) {
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
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size)
}
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMetaBinaries() {
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.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
writeBinary(binary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(keyBinary.binary)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
@@ -559,23 +548,22 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
}
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
private fun writeEntryBinaries(binaries: Map<String, BinaryAttachment>) {
for ((key, binary) in binaries) {
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(key))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
private fun writeEntryBinaries(binaries: LinkedHashMap<String, Int>) {
for ((label, poolId) in binaries) {
// Retrieve the right index with the poolId, don't use ref because of header in DatabaseV4
mDatabaseKDBX.binaryPool.getBinaryIndexFromKey(poolId)?.toString()?.let { indexString ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.startTag(null, DatabaseKDBXXML.ElemKey)
xml.text(safeXmlString(label))
xml.endTag(null, DatabaseKDBXXML.ElemKey)
xml.startTag(null, DatabaseKDBXXML.ElemValue)
val ref = mDatabaseKDBX.binaryPool.findKey(binary)
if (ref != null) {
xml.attribute(null, DatabaseKDBXXML.AttrRef, ref.toString())
} else {
writeBinary(binary)
xml.startTag(null, DatabaseKDBXXML.ElemValue)
// Use only pool data in Meta to save binaries
xml.attribute(null, DatabaseKDBXXML.AttrRef, indexString)
xml.endTag(null, DatabaseKDBXXML.ElemValue)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
xml.endTag(null, DatabaseKDBXXML.ElemValue)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
}

View File

@@ -95,9 +95,9 @@ open class Education(val activity: Activity) {
R.string.education_entry_edit_key,
R.string.education_password_generator_key,
R.string.education_entry_new_field_key,
R.string.education_add_attachment_key,
R.string.education_setup_OTP_key)
/**
* Get preferences bundle for education
*/
@@ -272,6 +272,18 @@ open class Education(val activity: Activity) {
context.resources.getBoolean(R.bool.education_entry_new_field_default))
}
/**
* Determines whether the explanatory view of the new attachment button in an entry has already been displayed.
*
* @param context The context to open the SharedPreferences
* @return boolean value of education_add_attachment_key key
*/
fun isEducationAddAttachmentPerformed(context: Context): Boolean {
val prefs = getEducationSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.education_add_attachment_key),
context.resources.getBoolean(R.bool.education_add_attachment_default))
}
/**
* Determines whether the explanatory view to setup OTP has already been displayed.
*

View File

@@ -29,6 +29,10 @@ import com.kunzisoft.keepass.R
class EntryEditActivityEducation(activity: Activity)
: Education(activity) {
/**
* Check and display learning views
* Displays the explanation for the password generator
*/
fun checkAndPerformedGeneratePasswordEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
@@ -56,7 +60,7 @@ class EntryEditActivityEducation(activity: Activity)
/**
* Check and display learning views
* Displays the explanation for the icon selection, the password generator and for a new field
* Displays the explanation to create a new field
*/
fun checkAndPerformedEntryNewFieldEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
@@ -83,6 +87,35 @@ class EntryEditActivityEducation(activity: Activity)
R.string.education_entry_new_field_key)
}
/**
* Check and display learning views
* Displays the explanation for to upload attachment
*/
fun checkAndPerformedAttachmentEducation(educationView: View,
onEducationViewClick: ((TapTargetView?) -> Unit)? = null,
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
return checkAndPerformedEducation(!isEducationAddAttachmentPerformed(activity),
TapTarget.forView(educationView,
activity.getString(R.string.education_add_attachment_title),
activity.getString(R.string.education_add_attachment_summary))
.textColorInt(Color.WHITE)
.tintTarget(true)
.cancelable(true),
object : TapTargetView.Listener() {
override fun onTargetClick(view: TapTargetView) {
super.onTargetClick(view)
onEducationViewClick?.invoke(view)
}
override fun onOuterCircleClick(view: TapTargetView?) {
super.onOuterCircleClick(view)
view?.dismiss(false)
onOuterViewClick?.invoke(view)
}
},
R.string.education_add_attachment_key)
}
/**
* Check and display learning views
* Displays the explanation to setup OTP

View File

@@ -21,24 +21,25 @@ package com.kunzisoft.keepass.model
import android.os.Parcel
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.writeEnum
data class EntryAttachment(var name: String,
var binaryAttachment: BinaryAttachment,
var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable {
data class EntryAttachmentState(var attachment: Attachment,
var streamDirection: StreamDirection,
var downloadState: AttachmentState = AttachmentState.NULL,
var downloadProgression: Int = 0) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readParcelable(BinaryAttachment::class.java.classLoader) ?: BinaryAttachment(),
parcel.readParcelable(Attachment::class.java.classLoader) ?: Attachment("", BinaryAttachment()),
parcel.readEnum<StreamDirection>() ?: StreamDirection.DOWNLOAD,
parcel.readEnum<AttachmentState>() ?: AttachmentState.NULL,
parcel.readInt())
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeParcelable(binaryAttachment, flags)
parcel.writeParcelable(attachment, flags)
parcel.writeEnum(streamDirection)
parcel.writeEnum(downloadState)
parcel.writeInt(downloadProgression)
}
@@ -49,26 +50,23 @@ data class EntryAttachment(var name: String,
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EntryAttachment) return false
if (other !is EntryAttachmentState) return false
if (name != other.name) return false
if (binaryAttachment != other.binaryAttachment) return false
if (attachment != other.attachment) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + binaryAttachment.hashCode()
return result
return attachment.hashCode()
}
companion object CREATOR : Parcelable.Creator<EntryAttachment> {
override fun createFromParcel(parcel: Parcel): EntryAttachment {
return EntryAttachment(parcel)
companion object CREATOR : Parcelable.Creator<EntryAttachmentState> {
override fun createFromParcel(parcel: Parcel): EntryAttachmentState {
return EntryAttachmentState(parcel)
}
override fun newArray(size: Int): Array<EntryAttachment?> {
override fun newArray(size: Int): Array<EntryAttachmentState?> {
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 androidx.documentfile.provider.DocumentFile
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.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.utils.UriUtil
import kotlinx.coroutines.*
import java.io.BufferedInputStream
import java.util.*
import kotlin.collections.HashMap
import java.util.concurrent.CopyOnWriteArrayList
class AttachmentFileNotificationService: LockNotificationService() {
override val notificationId: Int = 10000
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
@@ -51,33 +58,31 @@ class AttachmentFileNotificationService: LockNotificationService() {
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.add(actionTaskListener)
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
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)
}
}
}
})
attachmentNotificationList.forEach {
it.attachmentFileAction?.listener = attachmentFileActionListener
}
}
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = null
}
})
attachmentNotificationList.forEach {
it.attachmentFileAction?.listener = null
}
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 {
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
}
override fun onBind(intent: Intent): IBinder? {
@@ -87,49 +92,29 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
intent.getParcelableExtra(FILE_URI_KEY)
} else null
when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.UPLOAD)
}
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
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)
}
}
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.DOWNLOAD)
}
else -> {
if (downloadFileUri != null) {
downloadFileUris[downloadFileUri]?.notificationId?.let {
notificationManager?.cancel(it)
downloadFileUris.remove(downloadFileUri)
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
notificationManager?.cancel(elementToRemove.notificationId)
attachmentNotificationList.remove(elementToRemove)
}
}
if (downloadFileUris.isEmpty()) {
if (attachmentNotificationList.isEmpty()) {
stopSelf()
}
}
@@ -138,25 +123,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
return START_REDELIVER_INTENT
}
@Synchronized
fun checkCurrentAttachmentProgress() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
}
attachmentNotificationList.forEach { attachmentNotification ->
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentAction(
attachmentNotification.uri,
attachmentNotification.entryAttachmentState
)
}
})
}
}
private fun newNotification(downloadFileUri: Uri,
entryAttachment: EntryAttachment,
notificationIdAttachment: Int) {
@Synchronized
fun removeAttachmentAction(entryAttachment: EntryAttachmentState) {
attachmentNotificationList.firstOrNull {
it.entryAttachmentState == entryAttachment
}?.let {
attachmentNotificationList.remove(it)
}
}
private fun newNotification(attachmentNotification: AttachmentNotification) {
val pendingContentIntent = PendingIntent.getActivity(this,
0,
Intent().apply {
action = Intent.ACTION_VIEW
setDataAndType(downloadFileUri, contentResolver.getType(downloadFileUri))
setDataAndType(attachmentNotification.uri,
contentResolver.getType(attachmentNotification.uri))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}, PendingIntent.FLAG_CANCEL_CURRENT)
@@ -164,54 +159,84 @@ class AttachmentFileNotificationService: LockNotificationService() {
0,
Intent(this, AttachmentFileNotificationService::class.java).apply {
// No action to delete the service
putExtra(DOWNLOAD_FILE_URI_KEY, downloadFileUri)
putExtra(FILE_URI_KEY, attachmentNotification.uri)
}, PendingIntent.FLAG_CANCEL_CURRENT)
val fileName = DocumentFile.fromSingleUri(this, downloadFileUri)?.name ?: ""
val fileName = DocumentFile.fromSingleUri(this, attachmentNotification.uri)?.name ?: ""
val builder = buildNewNotification().apply {
setSmallIcon(R.drawable.ic_file_download_white_24dp)
setContentTitle(getString(R.string.download_attachment, fileName))
when (attachmentNotification.entryAttachmentState.streamDirection) {
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)
when (entryAttachment.downloadState) {
when (attachmentNotification.entryAttachmentState.downloadState) {
AttachmentState.NULL, AttachmentState.START -> {
setContentText(getString(R.string.download_initialization))
setOngoing(true)
}
AttachmentState.IN_PROGRESS -> {
if (entryAttachment.downloadProgression > 100) {
if (attachmentNotification.entryAttachmentState.downloadProgression > 100) {
setContentText(getString(R.string.download_finalization))
} else {
setProgress(100, entryAttachment.downloadProgression, false)
setContentText(getString(R.string.download_progression, entryAttachment.downloadProgression))
setProgress(100,
attachmentNotification.entryAttachmentState.downloadProgression,
false)
setContentText(getString(R.string.download_progression,
attachmentNotification.entryAttachmentState.downloadProgression))
}
setOngoing(true)
}
AttachmentState.COMPLETE, AttachmentState.ERROR -> {
AttachmentState.COMPLETE -> {
setContentText(getString(R.string.download_complete))
setContentIntent(pendingContentIntent)
when (attachmentNotification.entryAttachmentState.streamDirection) {
StreamDirection.UPLOAD -> {
}
StreamDirection.DOWNLOAD -> {
setContentIntent(pendingContentIntent)
}
}
setDeleteIntent(pendingDeleteIntent)
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() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = null
notificationManager?.cancel(entry.value.notificationId)
}
})
attachmentNotificationList.forEach { attachmentNotification ->
attachmentNotification.attachmentFileAction?.listener = null
notificationManager?.cancel(attachmentNotification.notificationId)
}
attachmentNotificationList.clear()
super.onDestroy()
}
private data class AttachmentNotification(var notificationId: Int,
var entryAttachment: EntryAttachment,
var attachmentTask: AttachmentFileActionClass? = null) {
private data class AttachmentNotification(var uri: Uri,
var notificationId: Int,
var entryAttachmentState: EntryAttachmentState,
var attachmentFileAction: AttachmentFileAction? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -228,52 +253,85 @@ class AttachmentFileNotificationService: LockNotificationService() {
}
}
private class AttachmentFileActionClass(
private val fileUri: Uri,
private fun actionUploadOrDownload(downloadFileUri: 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 contentResolver: ContentResolver) {
private val updateMinFrequency = 1000
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() {
TimeoutHelper.temporarilyDisableTimeout()
// on pre execute
attachmentNotification.attachmentTask = this
attachmentNotification.entryAttachment.apply {
attachmentNotification.attachmentFileAction = this
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.START
downloadProgression = 0
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
listener?.onUpdate(attachmentNotification)
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<Boolean> = async {
var progressResult = true
try {
attachmentNotification.entryAttachment.apply {
attachmentNotification.entryAttachmentState.apply {
downloadState = AttachmentState.IN_PROGRESS
binaryAttachment.download(fileUri, contentResolver, 1024) { percent ->
// Publish progress
val currentTime = System.currentTimeMillis()
if (previousSaveTime + updateMinFrequency < currentTime) {
attachmentNotification.entryAttachment.apply {
downloadState = AttachmentState.IN_PROGRESS
downloadProgression = percent
when (streamDirection) {
StreamDirection.UPLOAD -> {
uploadToDatabase(
attachmentNotification.uri,
attachment.binaryAttachment,
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) {
Log.e(TAG, "Unable to upload or download file", e)
progressResult = false
}
progressResult
@@ -282,33 +340,95 @@ class AttachmentFileNotificationService: LockNotificationService() {
// on post execute
withContext(Dispatchers.Main) {
val result = asyncResult.await()
attachmentNotification.attachmentTask = null
attachmentNotification.entryAttachment.apply {
attachmentNotification.attachmentFileAction = null
attachmentNotification.entryAttachmentState.apply {
downloadState = if (result) AttachmentState.COMPLETE else AttachmentState.ERROR
downloadProgression = 100
}
onUpdate?.invoke(fileUri,
attachmentNotification.entryAttachment,
attachmentNotification.notificationId)
listener?.onUpdate(attachmentNotification)
TimeoutHelper.releaseTemporarilyDisableTimeout()
}
}
}
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 {
private val TAG = AttachmentFileActionClass::class.java.name
private val TAG = AttachmentFileAction::class.java.name
}
}
companion object {
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 DOWNLOAD_FILE_URI_KEY = "DOWNLOAD_FILE_URI_KEY"
const val FILE_URI_KEY = "FILE_URI_KEY"
const val ATTACHMENT_KEY = "ATTACHMENT_KEY"
private val downloadFileUris = HashMap<Uri, AttachmentNotification>()
}
}

View File

@@ -150,7 +150,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment() {
recycleBinGroupPref = findPreference(getString(R.string.recycle_bin_group_key))
// Recycle bin
if (mDatabase.allowRecycleBin) {
if (mDatabase.allowConfigurableRecycleBin) {
val recycleBinEnablePref: SwitchPreference? = findPreference(getString(R.string.recycle_bin_enable_key))
recycleBinEnablePref?.apply {
isChecked = mDatabase.isRecycleBinEnabled

View File

@@ -231,12 +231,6 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
}
fun isFullFilePathEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.full_file_path_enable_key),
context.resources.getBoolean(R.bool.full_file_path_enable_default))
}
fun getListSort(context: Context): SortNodeEnum {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.getString(context.getString(R.string.sort_node_key),

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.IBinder
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.Companion.ACTION_ATTACHMENT_FILE_START_DOWNLOAD
import com.kunzisoft.keepass.notifications.AttachmentFileNotificationService.Companion.ACTION_ATTACHMENT_FILE_START_UPLOAD
class AttachmentFileBinderManager(private val activity: FragmentActivity) {
@@ -43,8 +46,18 @@ class AttachmentFileBinderManager(private val activity: FragmentActivity) {
private var mServiceConnection: ServiceConnection? = null
private val mActionTaskListener = object: AttachmentFileNotificationService.ActionTaskListener {
override fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment) {
onActionTaskListener?.onAttachmentProgress(fileUri, attachment)
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
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
}
@Synchronized
fun consummeAttachmentAction(attachment: EntryAttachmentState) {
mBinder?.getService()?.removeAttachmentAction(attachment)
}
@Synchronized
private fun start(bundle: Bundle? = null, actionTask: String) {
activity.stopService(mIntentTask)
if (bundle != null)
mIntentTask.putExtras(bundle)
activity.runOnUiThread {
mIntentTask.action = actionTask
activity.startService(mIntentTask)
}
mIntentTask.action = actionTask
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,
entryAttachment: EntryAttachment) {
attachment: Attachment) {
start(Bundle().apply {
putParcelable(AttachmentFileNotificationService.DOWNLOAD_FILE_URI_KEY, downloadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, entryAttachment)
putParcelable(AttachmentFileNotificationService.FILE_URI_KEY, downloadFileUri)
putParcelable(AttachmentFileNotificationService.ATTACHMENT_KEY, attachment)
}, 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
fun <V : Parcelable> readStringParcelableMap(
parcel: Parcel, vClass: Class<V>): LinkedHashMap<String, V> {
@@ -74,6 +83,19 @@ object ParcelableUtil {
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
fun writeStringParcelableMap(dest: Parcel, map: LinkedHashMap<String, String>) {

View File

@@ -27,10 +27,7 @@ import android.os.Build
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.*
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, "rwt")
else -> null
}
}
@Throws(FileNotFoundException::class)
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
if (fileUri == null)

View File

@@ -19,7 +19,6 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
@@ -28,7 +27,6 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
@@ -37,9 +35,11 @@ import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.DateInstant
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.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.OtpType
import java.util.*
@@ -52,27 +52,14 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private var fontInVisibility: Boolean = false
private val userNameContainerView: View
private val userNameView: TextView
private val userNameActionView: ImageView
private val passwordContainerView: View
private val passwordView: TextView
private val passwordActionView: ImageView
private val otpContainerView: View
private val otpLabelView: TextView
private val otpView: TextView
private val otpActionView: ImageView
private val userNameFieldView: EntryField
private val passwordFieldView: EntryField
private val otpFieldView: EntryField
private val urlFieldView: EntryField
private val notesFieldView: EntryField
private var otpRunnable: Runnable? = null
private val urlContainerView: View
private val urlView: TextView
private val notesContainerView: View
private val notesView: TextView
private val extraFieldsContainerView: View
private val extraFieldsListView: ViewGroup
@@ -84,7 +71,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val attachmentsContainerView: View
private val attachmentsListView: RecyclerView
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, false)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private val historyContainerView: View
private val historyListView: RecyclerView
@@ -93,34 +80,26 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val uuidView: TextView
private val uuidReferenceView: TextView
val isUserNamePresent: Boolean
get() = userNameContainerView.visibility == View.VISIBLE
val isPasswordPresent: Boolean
get() = passwordContainerView.visibility == View.VISIBLE
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_entry_contents, this)
userNameContainerView = findViewById(R.id.entry_user_name_container)
userNameView = findViewById(R.id.entry_user_name)
userNameActionView = findViewById(R.id.entry_user_name_action_image)
userNameFieldView = findViewById(R.id.entry_user_name_field)
userNameFieldView.setLabel(R.string.entry_user_name)
passwordContainerView = findViewById(R.id.entry_password_container)
passwordView = findViewById(R.id.entry_password)
passwordActionView = findViewById(R.id.entry_password_action_image)
passwordFieldView = findViewById(R.id.entry_password_field)
passwordFieldView.setLabel(R.string.entry_password)
otpContainerView = findViewById(R.id.entry_otp_container)
otpLabelView = findViewById(R.id.entry_otp_label)
otpView = findViewById(R.id.entry_otp)
otpActionView = findViewById(R.id.entry_otp_action_image)
otpFieldView = findViewById(R.id.entry_otp_field)
otpFieldView.setLabel(R.string.entry_otp)
urlContainerView = findViewById(R.id.entry_url_container)
urlView = findViewById(R.id.entry_url)
urlFieldView = findViewById(R.id.entry_url_field)
urlFieldView.setLabel(R.string.entry_url)
urlFieldView.setValueAutoLink(true)
notesContainerView = findViewById(R.id.entry_notes_container)
notesView = findViewById(R.id.entry_notes)
notesFieldView = findViewById(R.id.entry_notes_field)
notesFieldView.setLabel(R.string.entry_notes)
notesFieldView.setValueAutoLink(true)
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
extraFieldsListView = findViewById(R.id.extra_fields_list)
@@ -128,7 +107,7 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
attachmentsListView = findViewById(R.id.entry_attachments_list)
attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
@@ -154,86 +133,52 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
this.fontInVisibility = fontInVisibility
}
fun assignUserName(userName: String?) {
if (userName != null && userName.isNotEmpty()) {
userNameContainerView.visibility = View.VISIBLE
userNameView.apply {
text = userName
if (fontInVisibility)
applyFontVisibility()
}
} else {
userNameContainerView.visibility = View.GONE
}
}
fun assignUserNameCopyListener(onClickListener: OnClickListener) {
userNameActionView.setOnClickListener(onClickListener)
}
fun assignPassword(password: String?, allowCopyPassword: Boolean) {
if (password != null && password.isNotEmpty()) {
passwordContainerView.visibility = View.VISIBLE
passwordView.apply {
text = password
if (fontInVisibility)
applyFontVisibility()
}
passwordActionView.isActivated = !allowCopyPassword
} else {
passwordContainerView.visibility = View.GONE
}
}
fun assignPasswordCopyListener(onClickListener: OnClickListener?) {
passwordActionView.apply {
setOnClickListener(onClickListener)
if (onClickListener == null) {
fun assignUserName(userName: String?,
onClickListener: OnClickListener?) {
userNameFieldView.apply {
if (userName != null && userName.isNotEmpty()) {
visibility = View.VISIBLE
setValue(userName)
applyFontVisibility(fontInVisibility)
} else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
}
}
fun atLeastOneFieldProtectedPresent(): Boolean {
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryExtraField)
if (childCustomView.isProtected)
return true
}
}
return false
}
fun setHiddenPasswordStyle(hiddenStyle: Boolean) {
passwordView.applyHiddenStyle(hiddenStyle)
// Hidden style for custom fields
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryExtraField)
childCustomView.setHiddenPasswordStyle(hiddenStyle)
fun assignPassword(password: String?,
allowCopyPassword: Boolean,
onClickListener: OnClickListener?) {
passwordFieldView.apply {
if (password != null && password.isNotEmpty()) {
visibility = View.VISIBLE
setValue(password, true)
applyFontVisibility(fontInVisibility)
activateCopyButton(allowCopyPassword)
}else {
visibility = View.GONE
}
assignCopyButtonClickListener(onClickListener)
}
}
fun assignOtp(otpElement: OtpElement?,
otpProgressView: ProgressBar?,
onClickListener: OnClickListener) {
otpContainerView.removeCallbacks(otpRunnable)
otpFieldView.removeCallbacks(otpRunnable)
if (otpElement != null) {
otpContainerView.visibility = View.VISIBLE
otpFieldView.visibility = View.VISIBLE
if (otpElement.token.isEmpty()) {
otpView.text = context.getString(R.string.error_invalid_OTP)
otpActionView.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
assignOtpCopyListener(null)
otpFieldView.setValue(R.string.error_invalid_OTP)
otpFieldView.activateCopyButton(false)
otpFieldView.assignCopyButtonClickListener(null)
} else {
assignOtpCopyListener(onClickListener)
otpView.text = otpElement.token
otpLabelView.text = otpElement.type.name
otpFieldView.setLabel(otpElement.type.name)
otpFieldView.setValue(otpElement.token)
otpFieldView.assignCopyButtonClickListener(onClickListener)
when (otpElement.type) {
// Only add token if HOTP
@@ -249,47 +194,41 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
otpRunnable = Runnable {
if (otpElement.shouldRefreshToken()) {
otpView.text = otpElement.token
otpFieldView.setValue(otpElement.token)
}
otpProgressView?.progress = otpElement.secondsRemaining
otpContainerView.postDelayed(otpRunnable, 1000)
otpFieldView.postDelayed(otpRunnable, 1000)
}
otpContainerView.post(otpRunnable)
otpFieldView.post(otpRunnable)
}
}
}
} else {
otpContainerView.visibility = View.GONE
otpFieldView.visibility = View.GONE
otpProgressView?.visibility = View.GONE
}
}
fun assignOtpCopyListener(onClickListener: OnClickListener?) {
otpActionView.setOnClickListener(onClickListener)
}
fun assignURL(url: String?) {
if (url != null && url.isNotEmpty()) {
urlContainerView.visibility = View.VISIBLE
urlView.text = url
} else {
urlContainerView.visibility = View.GONE
urlFieldView.apply {
if (url != null && url.isNotEmpty()) {
visibility = View.VISIBLE
setValue(url)
} else {
visibility = View.GONE
}
}
}
fun assignComment(comment: String?) {
if (comment != null && comment.isNotEmpty()) {
notesContainerView.visibility = View.VISIBLE
notesView.apply {
text = comment
if (fontInVisibility)
applyFontVisibility()
fun assignNotes(notes: String?) {
notesFieldView.apply {
if (notes != null && notes.isNotEmpty()) {
visibility = View.VISIBLE
setValue(notes)
applyFontVisibility(fontInVisibility)
} else {
visibility = View.GONE
}
try {
Linkify.addLinks(notesView, Linkify.ALL)
} catch (e: Exception) {}
} else {
notesContainerView.visibility = View.GONE
}
}
@@ -322,6 +261,19 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
uuidReferenceView.text = UuidUtil.toHexString(uuid)
}
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
// Hidden style for custom fields
extraFieldsListView.let {
for (i in 0 until it.childCount) {
val childCustomView = it.getChildAt(i)
if (childCustomView is EntryField)
childCustomView.hiddenProtectedValue = hiddenProtectedValue
}
}
}
/* -------------
* Extra Fields
* -------------
@@ -334,14 +286,15 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
fun addExtraField(title: String,
value: ProtectedString,
allowCopy: Boolean,
onActionClickListener: OnClickListener?) {
onCopyButtonClickListener: OnClickListener?) {
val entryCustomField: EntryExtraField? = EntryExtraField(context, attrs, defStyle)
val entryCustomField: EntryField? = EntryField(context, attrs, defStyle)
entryCustomField?.apply {
setLabel(title)
setValue(value.toString(), value.isProtected)
activateActionButton(allowCopy)
assignActionButtonClickListener(onActionClickListener)
setValueAutoLink(true)
activateCopyButton(allowCopy)
assignCopyButtonClickListener(onCopyButtonClickListener)
applyFontVisibility(fontInVisibility)
}
entryCustomField?.let {
@@ -364,17 +317,18 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
}
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onAttachmentClicked: (attachment: EntryAttachment)->Unit) {
fun assignAttachments(attachments: Set<Attachment>,
streamDirection: StreamDirection,
onAttachmentClicked: (attachment: Attachment)->Unit) {
showAttachments(attachments.isNotEmpty())
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
attachmentsAdapter.onItemClickListener = { item ->
onAttachmentClicked.invoke(item)
onAttachmentClicked.invoke(item.attachment)
}
}
fun updateAttachmentDownloadProgress(attachmentToDownload: EntryAttachment) {
attachmentsAdapter.updateProgress(attachmentToDownload)
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
attachmentsAdapter.putItem(attachmentToDownload)
}
/* -------------

View File

@@ -33,14 +33,16 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryExtraFieldsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.assignDatabaseIcon
import com.kunzisoft.keepass.icons.assignDefaultDatabaseIcon
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.model.FocusedEditField
import com.kunzisoft.keepass.model.StreamDirection
import org.joda.time.Duration
import org.joda.time.Instant
@@ -68,7 +70,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
private val attachmentsListView: RecyclerView
private val extraFieldsAdapter = EntryExtraFieldsItemsAdapter(context)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context, true)
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
private var iconColor: Int = 0
private var expiresInstant: DateInstant = DateInstant(Instant.now().plus(Duration.standardDays(30)).toDate())
@@ -124,7 +126,7 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
}
}
attachmentsListView?.apply {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
adapter = attachmentsAdapter
(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
}
@@ -254,10 +256,13 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
/**
* Remove all children and add new views for each field
*/
fun assignExtraFields(fields: List<Field>, focusedExtraField: FocusedEditField? = null) {
fun assignExtraFields(fields: List<Field>,
onEditButtonClickListener: ((item: Field)->Unit)?,
focusedExtraField: FocusedEditField? = null) {
extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE
// Reinit focused field
extraFieldsAdapter.assignItems(fields, focusedExtraField)
extraFieldsAdapter.onEditButtonClickListener = onEditButtonClickListener
}
/**
@@ -273,20 +278,74 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
extraFieldsAdapter.putItem(extraField)
}
fun replaceExtraField(oldExtraField: Field, newExtraField: Field) {
extraFieldsContainerView.visibility = View.VISIBLE
extraFieldsAdapter.replaceItem(oldExtraField, newExtraField)
}
fun removeExtraField(oldExtraField: Field) {
extraFieldsAdapter.removeItem(oldExtraField)
}
fun getExtraFieldViewPosition(field: Field, position: (Float) -> Unit) {
extraFieldsListView.post {
position.invoke(extraFieldsListView.y
+ (extraFieldsListView.getChildAt(extraFieldsAdapter.indexOf(field))?.y
?: 0F)
)
}
}
/* -------------
* Attachments
* -------------
*/
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
onDeleteItem: (attachment: EntryAttachment)->Unit) {
fun getAttachments(): List<Attachment> {
return attachmentsAdapter.itemsList.map { it.attachment }
}
fun assignAttachments(attachments: Set<Attachment>,
streamDirection: StreamDirection,
onDeleteItem: (attachment: Attachment)->Unit) {
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
attachmentsAdapter.assignItems(attachments)
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
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()
}
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
attachmentsListView.postDelayed({
position.invoke(attachmentsContainerView.y
+ attachmentsListView.y
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
?: 0F)
)
}, 250)
}
/**
* Validate or not the entry form
*

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2019 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.kunzisoft.keepass.R
class EntryExtraField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private val labelView: TextView
private val valueView: TextView
private val actionImageView: ImageView
var isProtected = false
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_extra_field, this)
labelView = findViewById(R.id.title)
valueView = findViewById(R.id.value)
actionImageView = findViewById(R.id.action_image)
}
fun applyFontVisibility(fontInVisibility: Boolean) {
if (fontInVisibility)
valueView.applyFontVisibility()
}
fun setLabel(label: String?) {
labelView.text = label ?: ""
}
fun setValue(value: String?, isProtected: Boolean = false) {
valueView.text = value ?: ""
this.isProtected = isProtected
}
fun setHiddenPasswordStyle(hiddenStyle: Boolean) {
valueView.applyHiddenStyle(isProtected && hiddenStyle)
}
fun activateActionButton(enable: Boolean) {
// Reverse because isActivated show custom color and allow click
actionImageView.isActivated = !enable
}
fun assignActionButtonClickListener(onClickActionListener: OnClickListener?) {
actionImageView.setOnClickListener(onClickActionListener)
actionImageView.visibility = if (onClickActionListener == null) GONE else VISIBLE
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.view
import android.content.Context
import android.text.util.Linkify
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.text.util.LinkifyCompat
import com.kunzisoft.keepass.R
class EntryField @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: LinearLayout(context, attrs, defStyle) {
private val labelView: TextView
private val valueView: TextView
private val showButtonView: ImageView
private val copyButtonView: ImageView
private var isProtected = false
var hiddenProtectedValue: Boolean
get() {
return showButtonView.isSelected
}
set(value) {
showButtonView.isSelected = !value
changeProtectedValueParameters()
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.item_entry_field, this)
labelView = findViewById(R.id.entry_field_label)
valueView = findViewById(R.id.entry_field_value)
showButtonView = findViewById(R.id.entry_field_show)
copyButtonView = findViewById(R.id.entry_field_copy)
copyButtonView.visibility = View.GONE
}
fun applyFontVisibility(fontInVisibility: Boolean) {
if (fontInVisibility)
valueView.applyFontVisibility()
}
fun setLabel(label: String?) {
labelView.text = label ?: ""
}
fun setLabel(@StringRes labelId: Int) {
labelView.setText(labelId)
}
fun setValue(value: String?,
isProtected: Boolean = false) {
valueView.text = value ?: ""
this.isProtected = isProtected
showButtonView.visibility = if (isProtected) View.VISIBLE else View.GONE
showButtonView.setOnClickListener {
showButtonView.isSelected = !showButtonView.isSelected
changeProtectedValueParameters()
}
changeProtectedValueParameters()
}
fun setValue(@StringRes valueId: Int,
isProtected: Boolean = false) {
setValue(resources.getString(valueId), isProtected)
}
private fun changeProtectedValueParameters() {
valueView.apply {
if (isProtected) {
isFocusable = false
} else {
setTextIsSelectable(true)
}
applyHiddenStyle(isProtected && !showButtonView.isSelected)
if (valueView.autoLinkMask == Linkify.ALL) {
try {
LinkifyCompat.addLinks(this, Linkify.ALL)
} catch (e: Exception) {}
}
}
}
fun setValueAutoLink(autoLink: Boolean) {
valueView.autoLinkMask = if (autoLink && !isProtected) Linkify.ALL else 0
changeProtectedValueParameters()
}
fun activateCopyButton(enable: Boolean) {
// Reverse because isActivated show custom color and allow click
copyButtonView.isActivated = !enable
}
fun assignCopyButtonClickListener(onClickActionListener: OnClickListener?) {
copyButtonView.setOnClickListener(onClickActionListener)
copyButtonView.visibility = if (onClickActionListener == null) GONE else VISIBLE
}
}

View File

@@ -44,13 +44,15 @@ fun TextView.applyFontVisibility() {
typeface = typeFace
}
fun TextView.applyHiddenStyle(hide: Boolean) {
fun TextView.applyHiddenStyle(hide: Boolean, changeMaxLines: Boolean = true) {
if (hide) {
transformationMethod = PasswordTransformationMethod.getInstance()
maxLines = 1
if (changeMaxLines)
maxLines = 1
} else {
transformationMethod = null
maxLines = 800
if (changeMaxLines)
maxLines = 800
}
}

View File

@@ -60,8 +60,12 @@ class FileDatabaseInfo : Serializable {
fun getModificationString(): String? {
return documentFile?.lastModified()?.let {
DateFormat.getDateTimeInstance()
.format(Date(it))
if (it != 0L) {
DateFormat.getDateTimeInstance()
.format(Date(it))
} else {
null
}
}
}
@@ -74,7 +78,6 @@ class FileDatabaseInfo : Serializable {
fun retrieveDatabaseAlias(alias: String): String? {
return when {
alias.isNotEmpty() -> alias
PreferencesUtil.isFullFilePathEnable(context) -> fileUri?.path
else -> if (exists) documentFile?.name else fileUri?.path
}
}

View File

@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_up_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

View File

@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M8.59,16.34l4.58,-4.59 -4.58,-4.59L10,5.75l6,6 -6,6z"/>
</vector>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_arrow_left_white_24dp"
android:fromDegrees="180"
android:toDegrees="180"
android:visible="true" />

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

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/background_button_color_secondary"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.97,0.89 1.66,0.89L22,21c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM9,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM14,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

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_selected="true"
android:drawable="@drawable/ic_visibility_white_24dp" />
<item android:state_selected="false"
android:drawable="@drawable/ic_visibility_off_white_24dp" />
</selector>

View File

@@ -69,9 +69,6 @@
android:id="@+id/entry_edit_bottom_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/Widget.MaterialComponents.BottomAppBar"
android:backgroundTint="?attr/colorAccent"
app:backgroundTint="?attr/colorAccent"
app:fabAlignmentMode="center"
app:hideOnScroll="false"
app:layout_scrollFlags="scroll|enterAlways"

View File

@@ -124,26 +124,13 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarAppearance"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_info_white_24dp"
app:navigationContentDescription="@string/about"
android:elevation="4dp"
app:layout_collapseMode="pin"
android:theme="?attr/toolbarHomeAppearance"
android:popupTheme="?attr/toolbarPopupAppearance" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
android:layout_gravity="top|start"
app:layout_collapseMode="pin">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/file_manager_explanation_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_info_white_24dp"
android:contentDescription="@string/about"
style="@style/KeepassDXStyle.ImageButton.Simple.Secondary"/>
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@@ -156,8 +156,7 @@
android:layout_height="?attr/actionBarSize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:theme="?attr/actionToolbarAppearance"
android:background="?attr/colorAccent" />
style="?attr/actionToolbarAppearance" />
<include
layout="@layout/view_button_lock"

View File

@@ -34,9 +34,11 @@
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/entry_custom_field_label_container"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_custom_field_delete">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/entry_custom_field_label"
@@ -49,6 +51,16 @@
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_custom_field_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_delete_white_24dp"
android:contentDescription="@string/menu_delete"
style="@style/KeepassDXStyle.ImageButton.Simple" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/entry_custom_field_protection"
style="@style/KeepassDXStyle.TextAppearance.Small"

View File

@@ -47,5 +47,7 @@
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground" />
android:background="?android:attr/windowBackground"
android:paddingBottom="?attr/actionBarSize"
android:clipToPadding="false" />
</FrameLayout>

View File

@@ -23,6 +23,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:targetApi="p"
android:id="@+id/item_attachment_container"
android:focusable="false"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -31,7 +32,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem"
@@ -70,40 +70,44 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="48dp"
android:layout_height="48dp">
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/item_attachment_delete_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_gravity="center"
android:contentDescription="@string/content_description_remove_field"
android:focusable="true"
android:src="@drawable/ic_content_delete_white_24dp"
style="@style/KeepassDXStyle.ImageButton.Simple" />
<FrameLayout
android:id="@+id/item_attachment_progress_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:layout_gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:src="@drawable/ic_file_download_white_24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_file_stream_white_24dp"
android:contentDescription="@string/download"
android:tint="?attr/colorAccent" />
style="@style/KeepassDXStyle.ImageButton.Simple" />
<ProgressBar
android:id="@+id/item_attachment_progress"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
style="@style/KeepassDXStyle.ProgressBar.Circle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="36dp"
android:layout_height="36dp"
android:max="100"
android:progress="60" />
</FrameLayout>

View File

@@ -32,7 +32,7 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_delete">
app:layout_constraintEnd_toStartOf="@+id/entry_extra_field_edit">
<com.kunzisoft.keepass.view.EditTextSelectable
android:id="@+id/entry_extra_field_value"
@@ -46,14 +46,15 @@
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_extra_field_delete"
android:id="@+id/entry_extra_field_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_delete_white_24dp"
android:contentDescription="@string/menu_delete"
android:src="@drawable/ic_more_white_24dp"
android:contentDescription="@string/menu_edit"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,26 +24,34 @@
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/title"
android:id="@+id/entry_field_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action_image"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="title"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<TextView
android:id="@+id/value"
android:id="@+id/entry_field_value"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/entry_field_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action_image"
android:textIsSelectable="true"
app:layout_constraintEnd_toStartOf="@+id/entry_field_show"
tools:text="value"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/action_image"
android:id="@+id/entry_field_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_field_copy"
android:src="@drawable/ic_visibility_state"
android:contentDescription="@string/menu_showpass"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_field_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"

View File

@@ -176,38 +176,49 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/file_precise_info_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/file_delete_button">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification"
<LinearLayout
android:id="@+id/file_modification_container"
android:layout_width="0dp"
android:layout_height="match_parent"
tools:text="3:40:14 PM"
android:textColor="?android:attr/textColorHintInverse"
android:layout_height="wrap_content"
android:paddingStart="@dimen/default_margin"
android:paddingLeft="@dimen/default_margin"
android:paddingEnd="@dimen/default_margin"
android:paddingRight="@dimen/default_margin"
android:paddingBottom="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/file_size"
android:gravity="start|bottom"/>
android:orientation="vertical"
android:gravity="start|center_vertical" >
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entry_modified"
android:textColor="?android:attr/textColorHintInverse"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_modification"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Aug 21, 2020 3:40:14 PM"
android:textColor="?android:attr/textColorHintInverse"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/file_size"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="end|bottom"
android:layout_height="wrap_content"
android:gravity="end|center_vertical"
android:textColor="?android:attr/textColorHintInverse"
android:paddingBottom="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/file_modification"
app:layout_constraintStart_toEndOf="@+id/file_modification_container"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/default_margin"
android:layout_marginEnd="@dimen/default_margin"

View File

@@ -25,7 +25,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/KeepassDXStyle.Selectable.Item">
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
@@ -46,20 +46,26 @@
android:layout_marginEnd="@dimen/image_list_margin"
android:scaleType="fitXY"
android:src="@drawable/ic_blank_32dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="2dp"
android:paddingBottom="4dp"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_toRightOf="@+id/node_icon"
android:layout_toEndOf="@+id/node_icon">
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"
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
android:id="@+id/node_text"
android:layout_height="wrap_content"
@@ -78,5 +84,19 @@
android:singleLine="true"
style="@style/KeepassDXStyle.TextAppearance.Entry.SubTitle" />
</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"
style="@style/KeepassDXStyle.TextAppearance.Entry.Icon"
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>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/edit_text_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/edit_text_show">
<com.kunzisoft.keepass.view.EditTextSelectable
android:id="@+id/edit_text_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:focusable="true"
android:focusableInTouchMode="true"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/edit_text_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_visibility_state"
android:contentDescription="@string/menu_showpass"
style="@style/KeepassDXStyle.ImageButton.Simple"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -40,153 +40,41 @@
android:orientation="vertical">
<!-- Username -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_user_name_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_user_name_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_user_name_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_user_name_action_image"
android:text="@string/entry_user_name"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_user_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_user_name_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_user_name_action_image"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/entry_user_name_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- Password -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_password_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_password_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_password_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_password_action_image"
android:text="@string/entry_password"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_password_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_password_action_image"
android:focusable="false"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_password_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- OTP -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/entry_otp_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_otp_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_otp_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_otp_action_image"
android:text="@string/entry_otp"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_otp"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/entry_otp_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/entry_otp_action_image"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_otp_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_content_copy_white_24dp"
android:contentDescription="@string/menu_copy"
style="@style/KeepassDXStyle.ImageButton.Simple" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:visibility="gone" />
<!-- URL -->
<LinearLayout
android:id="@+id/entry_url_container"
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_url_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_url_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_url"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="all"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
android:visibility="gone" />
<!-- Comment -->
<LinearLayout
android:id="@+id/entry_notes_container"
<!-- Notes -->
<com.kunzisoft.keepass.view.EntryField
android:id="@+id/entry_notes_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_notes_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_notes"
android:autoLink="all"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
</LinearLayout>
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -190,7 +190,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:maxLines="20"
android:importantForAccessibility="no"
android:importantForAutofill="no"
android:inputType="textMultiLine"
@@ -214,6 +213,7 @@
android:id="@+id/extra_fields_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants"
android:layout_margin="@dimen/card_view_padding"
android:orientation="vertical">
</androidx.recyclerview.widget.RecyclerView>
@@ -247,7 +247,8 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_attachments_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -19,11 +19,6 @@
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_toggle_pass"
android:icon="@drawable/ic_visibility_white_24dp"
android:title="@string/menu_showpass"
android:orderInCategory="21"
app:showAsAction="always" />
<item android:id="@+id/menu_edit"
android:icon="@drawable/ic_mode_edit_white_24dp"
android:title="@string/menu_edit"

View File

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

View File

@@ -43,7 +43,7 @@
<string name="entry_title">العنوان</string>
<string name="entry_url">رابط</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_no_name">ادخل اسمًا.</string>
<string name="error_pass_match">كلمتا السر غير متطابقتين.</string>
@@ -189,8 +189,6 @@
<string name="unavailable_feature_version">نسخة الاندرويد %1$s لا تحقق ادنى متطلبات السنخة %2$s.</string>
<string name="file_name">اسم الملف</string>
<string name="path">مسار</string>
<string name="full_file_path_enable_title">مسار الملف</string>
<string name="full_file_path_enable_summary">عرض المسار الكامل للملف</string>
<string name="configure_biometric">فحص البصمة مدعوم لكنه ليس معد.</string>
<string name="open_biometric_prompt_unlock_database">فحص البصمة</string>
<string name="biometric_invalid_key">لا يمكن قراءة مفتاح البصمة.

View File

@@ -57,7 +57,7 @@
<string name="entry_user_name">Usuari</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_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_path">Assegureu-vos que el camí eś correcte.</string>
<string name="error_no_name">Introduïu-hi un nom.</string>
@@ -166,7 +166,6 @@
<string name="recycle_bin_title">Ús de la paperera</string>
<string name="database_data_compression_summary">La compressió de les dades redueix la mida de la base de dades.</string>
<string name="database_data_compression_title">Compressió de les dades</string>
<string name="full_file_path_enable_title">Ruta del fitxer</string>
<string name="assign_master_key">Assigna una contrasenya mestra</string>
<string name="path">Camí</string>
<string name="file_name">Nom de fitxer</string>

View File

@@ -59,8 +59,8 @@
<string name="entry_user_name">Uživatelské jméno</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_file_not_create">Soubor se nedaří vytvořit:</string>
<string name="error_invalid_db">Nelze přečíst databázi.</string>
<string name="error_file_not_create">Soubor se nedaří vytvořit</string>
<string name="error_invalid_db">Databázi nelze číst.</string>
<string name="error_invalid_path">Neplatná cesta.</string>
<string name="error_no_name">Vložte jméno.</string>
<string name="error_nokeyfile">Vyberte soubor s klíčem.</string>
@@ -169,7 +169,7 @@
<string name="menu_move">Přesunout</string>
<string name="menu_paste">Vložit</string>
<string name="menu_cancel">Storno</string>
<string name="menu_biometric_remove_key">Smaž uložený otisk prstu</string>
<string name="menu_biometric_remove_key">Smazat uložený biometrický klíč</string>
<string name="menu_file_selection_read_only">Chráněno před zápisem</string>
<string name="menu_open_file_read_and_write">Čtení a zápis</string>
<string name="read_only">Chráněno před zápisem</string>
@@ -214,7 +214,7 @@
<string name="list_password_generator_options_summary">Nastavit povolené znaky pro generátor hesel</string>
<string name="clipboard">Schránka</string>
<string name="clipboard_notifications_title">Oznamování schránky</string>
<string name="clipboard_notifications_summary">Zapnout oznamování schránky o kopírování pole při prohlížení záznamu</string>
<string name="clipboard_notifications_summary">Ukázat oznamení schránky o kopírování pole při prohlížení záznamu</string>
<string name="clipboard_warning">Vymazat historii schránky manuálně, pokud automatické vymazání schránky selže.</string>
<string name="lock">Zamknout</string>
<string name="lock_database_screen_off_title">Zámek obrazovky</string>
@@ -226,14 +226,12 @@
<string name="biometric_delete_all_key_summary">Smazat všechny šifrovací klíče související s biometrickým rozlišením</string>
<string name="biometric_delete_all_key_warning">Smazat všechny šifrovací klíče související s biometrickým rozlišením\?</string>
<string name="unavailable_feature_text">Tuto funkci se nedaří spustit.</string>
<string name="unavailable_feature_version">Verze %1$s vámi používaného systému Android nevyhovuje minimální verzi %2$s.</string>
<string name="unavailable_feature_version">V zařízení je instalován Android %1$s, ale potřebná je verze %2$s a novější.</string>
<string name="unavailable_feature_hardware">Hardware nebyl rozpoznán.</string>
<string name="file_name">Název souboru</string>
<string name="path">Cesta</string>
<string name="assign_master_key">Přiřadit hlavní klíč</string>
<string name="create_keepass_file">Vytvořit novou databázi</string>
<string name="full_file_path_enable_title">Cesta k souboru</string>
<string name="full_file_path_enable_summary">Zobrazit úplnou cestu k souboru</string>
<string name="recycle_bin_title">Využití koše</string>
<string name="recycle_bin_summary">Před smazáním přesune skupiny a položky do skupiny „Koš“</string>
<string name="monospace_font_fields_enable_title">Písmo položek</string>
@@ -251,7 +249,7 @@
<string name="magic_keyboard_title">Magikeyboard</string>
<string name="magic_keyboard_explanation_summary">Aktivovat vlastní klávesnici, která snadno vyplní hesla a další položky identity</string>
<string name="allow_no_password_title">Umožnit bez hlavního klíče</string>
<string name="allow_no_password_summary">Povolit tlačítko \"Otevřít\", i když není vybráno žádné heslo</string>
<string name="allow_no_password_summary">Povolit klepnutí na \"Otevřít\", i když není vybráno žádné heslo</string>
<string name="enable_read_only_title">Chráněno před zápisem</string>
<string name="enable_read_only_summary">Ve výchozím stavu otevřít databázi pouze pro čtení</string>
<string name="enable_education_screens_title">Vzdělávací nápovědy</string>
@@ -269,8 +267,8 @@
\nSkupiny (ekvivalent složek) organizují záznamy v databázi.</string>
<string name="education_search_title">Hledejte v položkách</string>
<string name="education_search_summary">Zadejte název, uživatelské jméno nebo jiné položky k nalezení svých hesel.</string>
<string name="education_biometric_title">Odemykání databáze otiskem prstu</string>
<string name="education_biometric_summary">Propojte své heslo a otisk prstu pro rychlé odemykání databáze.</string>
<string name="education_biometric_title">Odemknutí databáze biometricky</string>
<string name="education_biometric_summary">Propojte své heslo s načtenou biometrikou pro rychlé odemknutí databáze.</string>
<string name="education_entry_edit_title">Upravit položku</string>
<string name="education_entry_edit_summary">Přidejte ke své položce vlastní kolonky. Společná data mohou být sdílena mezi více různými kolonkami.</string>
<string name="education_generate_password_title">Vytvořit silné heslo</string>
@@ -419,7 +417,7 @@
<string name="database_custom_color_title">Vlastní barva databáze</string>
<string name="compression">Komprese</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="error_save_database">Nebylo možno uložit databázi.</string>
<string name="menu_save_database">Uložit databázi</string>
@@ -440,18 +438,18 @@
<string name="download_initialization">Zahajuji…</string>
<string name="download_progression">Probíhá: %1$d%%</string>
<string name="download_finalization">Dokončuji…</string>
<string name="download_complete">Ukončeno! Klepnout pro otevření souboru.</string>
<string name="download_complete">Kompletní!</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 nejsou ukázány</string>
<string name="contact">Kontakt</string>
<string name="contribution">Příspěvky</string>
<string name="feedback">Feedback</string>
<string name="auto_focus_search_title">Snadné hledání</string>
<string name="auto_focus_search_summary">Při otevření databáze žádat hledání</string>
<string name="remember_database_locations_title">Uložit umístění databází</string>
<string name="remember_database_locations_summary">Pamatovat si umístění databází</string>
<string name="remember_keyfile_locations_title">Uložit umístění souborů s klíči</string>
<string name="remember_keyfile_locations_summary">Pamatovat si umístění souborů s klíči</string>
<string name="remember_database_locations_title">Pamatovat si umístění databází</string>
<string name="remember_database_locations_summary">Uchová informaci o tom, kde jsou databáze uloženy</string>
<string name="remember_keyfile_locations_title">Pamatovat si umístění souborů s klíči</string>
<string name="remember_keyfile_locations_summary">Uchová informaci o tom, kde jsou uloženy soubory s klíči</string>
<string name="show_recent_files_title">Ukázat nedávné soubory</string>
<string name="show_recent_files_summary">Ukázat umístění nedávných databází</string>
<string name="hide_broken_locations_title">Skrýt špatné odkazy na databáze</string>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Brugernavn</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_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_path">Sørg for, at stien er korrekt.</string>
<string name="error_no_name">Indtast et navn.</string>
@@ -231,8 +231,6 @@
<string name="path">Sti</string>
<string name="assign_master_key">Tildel en hovednøgle</string>
<string name="create_keepass_file">Opret en ny database</string>
<string name="full_file_path_enable_title">Filsti</string>
<string name="full_file_path_enable_summary">Se den fulde filsti</string>
<string name="recycle_bin_title">Brug af papirkurven</string>
<string name="recycle_bin_summary">Flyt grupper og poster til gruppen \"Papirkurven\" før den slettes</string>
<string name="monospace_font_fields_enable_title">Feltskrifttype</string>
@@ -268,7 +266,7 @@
\nGrupper (~mapper) organiserer poster i databasen.</string>
<string name="education_search_title">Søg i poster</string>
<string name="education_search_summary">Indtast titel, brugernavn eller indhold af andre felter for at hente adgangskoder.</string>
<string name="education_biometric_title">Oplåsning af database ved hjælp af biometrisk</string>
<string name="education_biometric_title">Biometrisk oplåsning af databasen</string>
<string name="education_biometric_summary">Knyt adgangskoden til det scannede biometri for hurtigt at låse databasen op.</string>
<string name="education_entry_edit_title">Rediger posten</string>
<string name="education_entry_edit_summary">Rediger post med brugerdefinerede felter. Pool data kan refereres mellem forskellige indtastningsfelter.</string>
@@ -419,7 +417,7 @@
<string name="database_custom_color_title">Brugerdefineret databasefarve</string>
<string name="compression">Komprimering</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="error_save_database">Databasen kunne ikke gemmes.</string>
<string name="menu_save_database">Gem database</string>
@@ -440,9 +438,9 @@
<string name="download_initialization">Initialiserer…</string>
<string name="download_progression">I gang: %1$d%%</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_summary">Udløbne poster er skjult</string>
<string name="hide_expired_entries_summary">Udløbne poster vises ikke</string>
<string name="contact">Kontakt</string>
<string name="contribution">Bidrag</string>
<string name="feedback">Tilbagemelding</string>

View File

@@ -67,7 +67,7 @@
<string name="entry_user_name">Benutzername</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_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_path">Sicherstellen, dass der Pfad korrekt ist.</string>
<string name="error_no_name">Namen eingeben.</string>
@@ -170,8 +170,8 @@
<string name="menu_appearance_settings">Aussehen</string>
<string name="password_size_title">Generierte Passwortlänge</string>
<string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string>
<string name="clipboard_notifications_title">Zwischenablagenbenachrichtigungen</string>
<string name="clipboard_notifications_summary">Benachrichtigungen für die Zwischenablage einschalten, um beim Anzeigen von Eingabefeldern diese kopieren zu können</string>
<string name="clipboard_notifications_title">Zwischenablage-Benachrichtigung</string>
<string name="clipboard_notifications_summary">Benachrichtigungen zur Zwischenablage anzeigen, um beim Betrachten eines Eintrags Felder kopieren zu können</string>
<string name="lock_database_screen_off_title">Bildschirmsperre</string>
<string name="lock_database_screen_off_summary">Datenbank sperren, wenn der Bildschirm ausgeschaltet wird</string>
<string name="create_keepass_file">Neue Datenbank erstellen</string>
@@ -198,7 +198,7 @@
<string name="key_derivation_function">Schlüsselableitungsfunktion</string>
<string name="extended_ASCII">Erweiterte ASCII</string>
<string name="allow">Erlauben</string>
<string name="error_autofill_enable_service">Autofill-Dienst kann nicht aktiviert werden.</string>
<string name="error_autofill_enable_service">Dienst für automatisches Ausfüllen kann nicht aktiviert werden.</string>
<string name="copy_field">Kopie von %1$s</string>
<string name="menu_form_filling_settings">Formularausfüllung</string>
<string name="menu_biometric_remove_key">Gespeicherten biometrischen Schlüssel löschen</string>
@@ -218,19 +218,17 @@
<string name="sort_last_modify_time">Änderungsdatum</string>
<string name="sort_last_access_time">Zugriffsdatum</string>
<string name="biometric_not_recognized">Biometrische Daten nicht erkannt</string>
<string name="autofill">Autofill</string>
<string name="autofill">Automatisches Ausfüllen</string>
<string name="autofill_service_name">KeePassDX autom. Formularausfüllung</string>
<string name="autofill_sign_in_prompt">Mit KeePassDX anmelden</string>
<string name="set_autofill_service_title">Autofill-Dienstvorgabe festlegen</string>
<string name="autofill_explanation_summary">Autofill aktivieren, um automatisch Eingabefelder in anderen Apps auszufüllen</string>
<string name="set_autofill_service_title">Standarddienst für automatisches Ausfüllen festlegen</string>
<string name="autofill_explanation_summary">Automatisches Ausfüllen aktivieren, um schnell Formulare in anderen Apps auszufüllen</string>
<string name="clipboard">Zwischenablage</string>
<string name="biometric_delete_all_key_title">Verschlüsselungsschlüssel löschen</string>
<string name="biometric_delete_all_key_summary">Alle mit der biometrischen Erkennung verbundenen Verschlüsselungsschlüssel löschen.</string>
<string name="biometric_delete_all_key_warning">Sind Sie sicher, dass Sie alle mit der biometrischen Erkennung verknüpften Schlüssel löschen möchten\?</string>
<string name="unavailable_feature_version">Die Android-Version, %1$s, erfüllt nicht die Mindestanforderung für Version %2$s.</string>
<string name="unavailable_feature_version">Auf dem Gerät läuft Android %1$s, eine Version ab %2$s wäre aber nötig.</string>
<string name="unavailable_feature_hardware">Keine entsprechende Hardware.</string>
<string name="full_file_path_enable_title">Dateipfad</string>
<string name="full_file_path_enable_summary">Vollständigen Dateipfad anzeigen</string>
<string name="recycle_bin_title">Papierkorb-Nutzung</string>
<string name="recycle_bin_summary">Verschiebt Gruppen oder Einträge in den Papierkorb, bevor sie gelöscht werden.</string>
<string name="monospace_font_fields_enable_title">Feldschriftart</string>
@@ -259,7 +257,7 @@
\nGruppen (wie Ordner) helfen, Einträge in der Datenbank zu ordnen.</string>
<string name="education_search_title">Einträge durchsuchen</string>
<string name="education_search_summary">Titel, Benutzernamen oder Inhalte anderer Feldern eingeben, um die Passwörter wiederzufinden.</string>
<string name="education_biometric_title">Datenbank-Entsperrung durch Biometrie</string>
<string name="education_biometric_title">Biometrische Datenbank-Entsperrung</string>
<string name="education_biometric_summary">Verknüpft Ihr Passwort mit Ihrer gescannten Biometrie, um Ihre Datenbank schnell zu entsperren.</string>
<string name="education_entry_edit_title">Eintrag bearbeiten</string>
<string name="education_entry_edit_summary">Bearbeiten Sie Ihren Eintrag mit persönlichen Feldern. Persönliche Felder können mit Querverweisen aus anderen Einträgen ergänzt werden.</string>
@@ -304,7 +302,7 @@
<string name="clipboard_warning">Wenn das automatische Löschen der Zwischenablage fehlschlägt, bitte den Verlauf manuell löschen.</string>
<string name="allow_copy_password_warning">WARNUNG: Alle Apps teilen sich die Zwischenablage. Wenn sensible Daten kopiert werden, kann andere Software darauf zugreifen.</string>
<string name="allow_no_password_title">Entsperren ohne Hauptschlüssel</string>
<string name="allow_no_password_summary">„Öffnen“-Taste aktivieren, wenn keine Passwort-Identifikation festgelegt ist</string>
<string name="allow_no_password_summary">Erlaubt das Antippen der „Öffnen“-Taste, wenn keine Anmeldeinformationen festgelegt sind</string>
<string name="enable_education_screens_title">Hilfe-Anzeige</string>
<string name="enable_education_screens_summary">Bedienelemente hervorheben, um die Funktionsweise der App zu lernen</string>
<string name="menu_open_file_read_and_write">Änderbar</string>
@@ -341,10 +339,10 @@
<string name="keyboard_key_vibrate_title">Vibrierende Tastendrücke</string>
<string name="keyboard_key_sound_title">Hörbare Tastendrücke</string>
<string name="selection_mode">Auswahlmodus</string>
<string name="remember_database_locations_title">Speicherort der Datenbanken</string>
<string name="remember_database_locations_summary">Speicherort der Datenbanken merken</string>
<string name="remember_keyfile_locations_title">Speicherort der Schlüsseldateien</string>
<string name="remember_keyfile_locations_summary">Speicherort der Schlüssel zu Datenbanken merken</string>
<string name="remember_database_locations_title">Datenbank-Speicherorte merken</string>
<string name="remember_database_locations_summary">Verfolgt, wo Datenbanken gespeichert sind</string>
<string name="remember_keyfile_locations_title">Schlüsseldatei-Speicherorte merken</string>
<string name="remember_keyfile_locations_summary">Verfolgt, wo Schlüsseldateien gespeichert sind</string>
<string name="show_recent_files_title">Zuletzt verwendete Dateien anzeigen</string>
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
@@ -360,7 +358,7 @@
<string name="content_description_open_file">Datei öffnen</string>
<string name="content_description_add_entry">Eintrag hinzufügen</string>
<string name="content_description_add_group">Gruppe hinzufügen</string>
<string name="content_description_file_information">Datei-Informationen</string>
<string name="content_description_file_information">Datei-Info</string>
<string name="content_description_entry_icon">Symbol für den Eintrag</string>
<string name="entry_password_generator">Passwort-Generator</string>
<string name="content_description_password_length">Passwortlänge</string>
@@ -370,7 +368,7 @@
<string name="list_groups_show_number_entries_title">Anzahl der Einträge anzeigen</string>
<string name="list_groups_show_number_entries_summary">Anzahl der Einträge in einer Gruppe anzeigen</string>
<string name="content_description_add_node">Knoten hinzufügen</string>
<string name="lock_database_back_root_title">\"Zurück\" drücken, um zu sperren</string>
<string name="lock_database_back_root_title">Zurück drücken, um zu sperren</string>
<string name="clear_clipboard_notification_summary">Die Datenbank sperren, wenn die Dauer der Zwischenablage abläuft oder die Benachrichtigung geschlossen wird, nachdem Sie sie zu benutzen begonnen haben</string>
<string name="content_description_node_children">Untergeordneter Knotenpunkt</string>
<string name="content_description_keyfile_checkbox">Schlüsseldatei-Kontrollkästchen</string>
@@ -435,7 +433,7 @@
<string name="database_custom_color_title">Benutzerdefinierte Datenbankfarbe</string>
<string name="compression">Kompression</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="error_save_database">Die Datenbank konnte nicht gespeichert werden.</string>
<string name="menu_save_database">Datenbank speichern</string>
@@ -456,9 +454,9 @@
<string name="download_initialization">Initialisieren…</string>
<string name="download_progression">Fortschritt: %1$d%%</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_summary">Abgelaufene Einträge werden ausgeblendet</string>
<string name="hide_expired_entries_summary">Abgelaufene Einträge werden nicht angezeigt</string>
<string name="style_choose_title">App-Design</string>
<string name="style_choose_summary">App-Design, das in der App genutzt wird</string>
<string-array name="list_style_names">
@@ -472,27 +470,27 @@
</string-array>
<string name="warning_database_read_only">Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern</string>
<string name="education_setup_OTP_summary">Einrichten einer Einmal-Passwortverwaltung (HOTP / TOTP), um ein Token zu generieren, das für die Zwei-Faktor-Authentifizierung (2FA) angefordert wird.</string>
<string name="education_setup_OTP_title">Einrichten von OTP</string>
<string name="education_setup_OTP_title">OTP einrichten</string>
<string name="error_create_database">Es ist nicht möglich, eine Datenbankdatei zu erstellen.</string>
<string name="entry_add_attachment">Anhang hinzufügen</string>
<string name="discard">Verwerfen</string>
<string name="discard_changes">Änderungen verwerfen\?</string>
<string name="validate">Validieren</string>
<string name="autofill_auto_search_summary">Suchergebnisse aus der Web-Domain oder Anwendungs-ID automatisch vorschlagen</string>
<string name="autofill_auto_search_summary">Automatisch Suchergebnisse nach Web-Domain oder Anwendungs-ID vorschlagen</string>
<string name="autofill_auto_search_title">Automatische Suche</string>
<string name="lock_database_show_button_summary">Zeigt die Sperrtaste in der Benutzeroberfläche an</string>
<string name="lock_database_show_button_title">Sperrtaste anzeigen</string>
<string name="autofill_preference_title">Autofill-Einstellungen</string>
<string name="autofill_preference_title">Einstellungen für automatisches Ausfüllen</string>
<string name="warning_database_link_revoked">Zugriff auf die Datei durch den Dateimanager widerrufen</string>
<string name="error_label_exists">Diese Bezeichnung existiert bereits.</string>
<string name="keyboard_search_share_summary">Automatische Suche nach gemeinsam genutzten Informationen zur Belegung der Tastatur</string>
<string name="keyboard_search_share_title">Gemeinsame Infos durchsuchen</string>
<string name="autofill_block_restart">Starten Sie die Anwendung, die das Formular enthält, neu, um die Sperrung zu aktivieren.</string>
<string name="autofill_block">Automatisches Füllen blockieren</string>
<string name="autofill_web_domain_blocklist_summary">Sperrliste, die das automatische Einsetzen von Web-Domains verhindert</string>
<string name="autofill_web_domain_blocklist_title">Web-Domain-Sperrliste</string>
<string name="autofill_application_id_blocklist_summary">Sperrliste, die das automatische Füllen von Apps verhindert</string>
<string name="autofill_application_id_blocklist_title">Anwendungs-Sperrliste</string>
<string name="autofill_block">Automatisches Ausfüllen sperren</string>
<string name="autofill_web_domain_blocklist_summary">Liste der Domains, auf denen ein automatisches Ausfüllen unterbunden wird</string>
<string name="autofill_web_domain_blocklist_title">Liste gesperrter Web-Domains</string>
<string name="autofill_application_id_blocklist_summary">Liste der Apps, in denen ein automatisches Ausfüllen unterbunden wird</string>
<string name="autofill_application_id_blocklist_title">Liste gesperrter Anwendungen</string>
<string name="subdomain_search_summary">Suche Web-Domains mit Subdomain-Beschränkungen</string>
<string name="subdomain_search_title">Subdomain-Suche</string>
<string name="error_string_type">Dieser Text stimmt nicht mit dem angeforderten Element überein.</string>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Όνομα Χρήστη</string>
<string name="error_arc4">Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται.</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_path">Βεβαιωθείτε ότι η διαδρομή είναι σωστή.</string>
<string name="error_no_name">Εισαγάγετε ένα όνομα.</string>
@@ -203,8 +203,6 @@
<string name="path">Διαδρομή</string>
<string name="assign_master_key">Ορίστε ένα κύριο κλειδί</string>
<string name="create_keepass_file">Δημιουργία νέας βάσης δεδομένων</string>
<string name="full_file_path_enable_title">Διαδρομή αρχείου</string>
<string name="full_file_path_enable_summary">Προβολή ολόκληρης της διαδρομής αρχείου</string>
<string name="recycle_bin_title">Χρήση Κάδου ανακύκλωσης</string>
<string name="recycle_bin_summary">Μετακίνηση ομάδων και καταχωρίσεων στην ομάδα \"Κάδο ανακύκλωσης\" πριν την διαγραφή</string>
<string name="monospace_font_fields_enable_title">Γραμματοσειρά πεδίου</string>
@@ -418,7 +416,7 @@
<string name="database_custom_color_title">Προσαρμοσμένο χρώμα βάσης δεδομένων</string>
<string name="compression">Συμπίεση</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="device_keyboard_setting_title">Ρυθμίσεις πληκτρολογίου συσκευής</string>
<string name="education_biometric_title">Ξεκλείδωμα Βάσης Δεδομένων με βιομετρικά στοιχεία</string>
@@ -442,7 +440,7 @@
<string name="download_initialization">Αρχικοποίηση…</string>
<string name="download_progression">Σε εξέλιξη: %1$d%%</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_summary">Οι καταχωρίσεις που έχουν λήξει είναι κρυμμένες</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="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_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_path">Asegúrese de que la ruta sea correcta.</string>
<string name="error_no_name">Proporcione un nombre.</string>
@@ -220,8 +220,6 @@
<string name="path">Ruta</string>
<string name="assign_master_key">Asignar una clave maestra</string>
<string name="create_keepass_file">Crear base de datos nueva</string>
<string name="full_file_path_enable_title">Ruta de archivo</string>
<string name="full_file_path_enable_summary">Ver la ruta completa del archivo</string>
<string name="recycle_bin_title">Usar la papelera de reciclaje</string>
<string name="recycle_bin_summary">Mueva un grupo o una entrada a la papelera de reciclaje antes de eliminar</string>
<string name="monospace_font_fields_enable_title">Fuente de los campos</string>
@@ -397,7 +395,7 @@
<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="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_progression">En progreso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string>
@@ -407,7 +405,7 @@
<string name="autofill_block">Bloquear autocompletado</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="compression_gzip">gzip</string>
<string name="compression_gzip">Gzip</string>
<string name="compression_none">Ninguna</string>
<string name="compression">Compresión</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="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_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_path">Fitxategirako bide baliogabea.</string>
<string name="error_no_name">Izen bat behar da.</string>

View File

@@ -0,0 +1,261 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="menu_appearance_settings">ظاهر</string>
<string name="database_history">تاریخچه</string>
<string name="credential_before_click_biometric_button">رمز ورود را وارد کنید و سپس روی دکمه \"بیومتریک\" کلیک کنید.</string>
<string name="no_credentials_stored">این پایگاه داده هنوز اطلاعات کاربری ذخیره نشده است.</string>
<string name="biometric_scanning_error">خطای بیومتریک:٪ 1 $ s</string>
<string name="biometric_not_recognized">بایومتریک قابل تشخیص نیست</string>
<string name="biometric_invalid_key">"کلید بیومتریک را نمی توان خواند. لطفاً آن را حذف کرده و روش شناخت بیومتریک را تکرار کنید."</string>
<string name="encrypted_value_stored">رمز رمزگذاری شده ذخیره شده است</string>
<string name="biometric_prompt_extract_credential_message">استخراج اطلاعات کاربری پایگاه داده با داده های بیومتریک</string>
<string name="biometric_prompt_extract_credential_title">پایگاه داده را با تشخیص بیومتریک باز کنید</string>
<string name="biometric_prompt_store_credential_message">هشدار: اگر از تشخیص بیومتریک استفاده می کنید ، هنوز باید رمز عبور اصلی خود را به خاطر بسپارید.</string>
<string name="biometric_prompt_store_credential_title">تشخیص بیومتریک را ذخیره کنید</string>
<string name="open_biometric_prompt_store_credential">اعلان بیومتریک را برای ذخیره اعتبارنامه باز کنید</string>
<string name="open_biometric_prompt_unlock_database">برای باز کردن قفل پایگاه داده ، دستور بیومتریک را باز کنید</string>
<string name="keystore_not_accessible">فروشگاه اصلی به درستی تنظیم نشده است.</string>
<string name="configure_biometric">بیومتریک پشتیبانی می شود ، اما تنظیم نشده است.</string>
<string name="build_label">٪ 1 $ s را بسازید</string>
<string name="version_label">نسخه٪ 1 $ s</string>
<string name="warning_permanently_delete_nodes">گره های انتخاب شده برای همیشه حذف شوند؟</string>
<string name="warning_no_encryption_key">بدون کلید رمزگذاری ادامه می دهید؟</string>
<string name="warning_empty_password">بدون محافظت در مورد باز کردن قفل رمز عبور ادامه می دهید؟</string>
<string name="warning_database_link_revoked">دسترسی به پرونده توسط مدیر فایل لغو شده است</string>
<string name="warning_database_read_only">برای ذخیره تغییرات پایگاه داده ، اجازه دسترسی به نوشتن پرونده را بدهید</string>
<string name="warning_password_encoding">از نویسه های رمز عبور خارج از قالب رمزگذاری متن در پرونده پایگاه داده خودداری کنید (کاراکترهای شناخته نشده به همان حرف تبدیل می شوند).</string>
<string name="warning">هشدار</string>
<string name="uppercase">بزرگ</string>
<string name="unsupported_db_version">نسخه پایگاه داده پشتیبانی نمی شود.</string>
<string name="underline">زیر خط بزنید</string>
<string name="search_results">نتایج جستجو</string>
<string name="search">جستجو</string>
<string name="special">ویژه</string>
<string name="sort_last_access_time">دسترسی</string>
<string name="sort_last_modify_time">تغییر</string>
<string name="sort_creation_time">ایجاد</string>
<string name="sort_username">نام کاربری</string>
<string name="sort_title">عنوان</string>
<string name="sort_db">نظم طبیعی</string>
<string name="sort_recycle_bin_bottom">سطل بازیافت در پایین</string>
<string name="sort_groups_before">گروه های قبل</string>
<string name="sort_ascending">اول کمترین</string>
<string name="sort_menu">مرتب سازی</string>
<string name="search_label">جستجو</string>
<string name="space">فاصله</string>
<string name="do_not_kill_app">برنامه را نکشید</string>
<string name="command_execution">اجرای فرمان…</string>
<string name="saving_database">در حال ذخیره پایگاه داده</string>
<string name="parallelism_explanation">درجه موازی سازی (یعنی تعداد موضوعات) که توسط عملکرد مشتق کلیدی استفاده می شود.</string>
<string name="parallelism">موازی کاری</string>
<string name="memory_usage_explanation">مقدار حافظه (در بایت) که توسط تابع مشتق کلید مورد استفاده قرار گیرد.</string>
<string name="memory_usage">استفاده از حافظه</string>
<string name="rounds_explanation">دورهای رمزگذاری اضافی محافظت بالاتری در برابر حملات نیروی وحشی ایجاد می کنند ، اما در واقع می توانند سرعت و بارگذاری را کاهش دهند.</string>
<string name="rounds">دور تحول</string>
<string name="kdf_explanation">برای تولید کلید برای الگوریتم رمزگذاری ، کلید اصلی با استفاده از یک تابع مشتق کلید نمکی تصادفی تبدیل می شود.</string>
<string name="encryption_explanation">الگوریتم رمزنگاری پایگاه داده برای همه داده ها استفاده می شود.</string>
<string name="root">ریشه</string>
<string name="hide_broken_locations_summary">پنهان کردن لینک های شکسته در لیست پایگاه داده های اخیر</string>
<string name="hide_broken_locations_title">پیوندهای پایگاه داده خراب را پنهان کنید</string>
<string name="show_recent_files_summary">نمایش مکان های پایگاه داده های اخیر</string>
<string name="show_recent_files_title">نمایش پرونده های اخیر</string>
<string name="remember_keyfile_locations_summary">پیگیری محل ذخیره فایل های کلیدی را نگه می دارد</string>
<string name="remember_keyfile_locations_title">مکانهای پرونده اصلی را بخاطر بسپارید</string>
<string name="remember_database_locations_summary">پیگیری محل ذخیره پایگاه داده ها را نگه می دارد</string>
<string name="remember_database_locations_title">مکان های پایگاه داده ها را به یاد داشته باشید</string>
<string name="selection_mode">حالت انتخاب</string>
<string name="contains_duplicate_uuid_procedure">با ایجاد UUID جدید برای ادامه نسخه ها ، مشکل حل می شود؟</string>
<string name="contains_duplicate_uuid">پایگاه داده شامل UUIDs تکراری است.</string>
<string name="read_only_warning">بسته به مدیر فایل شما، KeePassDX ممکن است اجازه نوشتن در ذخیره سازی شما را ندارد.</string>
<string name="read_only">محافظت از نوشتن</string>
<string name="protection">حفاظت</string>
<string name="progress_title">کار کردن…</string>
<string name="progress_create">ایجاد پایگاه داده جدید…</string>
<string name="subdomain_search_summary">جستجوی دامنه های وب با محدودیت های زیر دامنه ها</string>
<string name="subdomain_search_title">جستجوی زیردریایی</string>
<string name="auto_focus_search_summary">درخواست جستجو در هنگام باز کردن یک پایگاه داده</string>
<string name="auto_focus_search_title">جستجوی سریع</string>
<string name="omit_backup_search_summary">Omits \"پشتیبان گیری\" و \"سطل بازیافت\" گروه از نتایج جستجو</string>
<string name="omit_backup_search_title">از طریق ورودی های پشتیبان جستجو نکنید</string>
<string name="create_keepass_file">ایجاد پایگاه داده جدید</string>
<string name="select_database_file">باز کردن پایگاه داده موجود</string>
<string name="no_url_handler">برای باز کردن این URL یک مرورگر وب نصب کنید.</string>
<string name="no_results">بدون نتایج جستجو</string>
<string name="never">هرگز</string>
<string name="minus">منهای</string>
<string name="menu_delete_entry_history">حذف تاریخچه</string>
<string name="menu_restore_entry_history">تاریخ را بازیابی کنید</string>
<string name="menu_empty_recycle_bin">سطل بازیافت را خالی کن</string>
<string name="menu_open_file_read_and_write">قابل تغییر</string>
<string name="menu_file_selection_read_only">نوشتن-محافظت شده</string>
<string name="menu_url">رفتن به آدرس اینترنتی</string>
<string name="menu_biometric_remove_key">حذف کلید بیومتریک ذخیره شده</string>
<string name="menu_showpass">نمایش رمز عبور</string>
<string name="menu_search">جستجو</string>
<string name="menu_open">باز</string>
<string name="menu_save_database">ذخیره پایگاه داده</string>
<string name="menu_lock">بانک اطلاعاتی قفل</string>
<string name="menu_hide_password">پنهان کردن رمز عبور</string>
<string name="menu_cancel">لغو</string>
<string name="menu_delete">حذف</string>
<string name="menu_paste">جاگذاری</string>
<string name="menu_move">حرکت</string>
<string name="menu_copy">کپی</string>
<string name="menu_edit">ویرایش</string>
<string name="menu_donate">اهدا کنید</string>
<string name="menu_master_key_settings">تنظیمات کلید اصلی</string>
<string name="menu_security_settings">تنظیمات امنیتی</string>
<string name="menu_database_settings">تنظیمات پایگاه داده</string>
<string name="menu_advanced_unlock_settings">باز کردن قفل پیشرفته</string>
<string name="menu_form_filling_settings">پر کردن فرم</string>
<string name="menu_app_settings">تنظیمات برنامه</string>
<string name="settings">تنظیمات</string>
<string name="copy_field">کپی %1$s</string>
<string name="menu_change_key_settings">کلید اصلی را تغییر دهید</string>
<string name="about">در باره</string>
<string name="hide_password_summary">رمزهای عبور ماسک (***) به طور پیش فرض</string>
<string name="hide_password_title">پنهان کردن رمزهای عبور</string>
<string name="lowercase">ترجمه</string>
<string name="loading_database">پایگاه داده بارگذاری…</string>
<string name="creating_database">ایجاد پایگاه داده…</string>
<string name="list_size_summary">اندازه متن در لیست عناصر</string>
<string name="list_size_title">اندازه موارد لیست</string>
<string name="list_groups_show_number_entries_summary">نمایش تعداد ورودی ها در یک گروه</string>
<string name="list_groups_show_number_entries_title">نمایش تعداد ورودی ها</string>
<string name="list_entries_show_username_summary">نمایش نام های کاربری در لیست های ورودی</string>
<string name="list_entries_show_username_title">نمایش نام های کاربری</string>
<string name="length">طول</string>
<string name="keyfile_is_empty">فایل کلید خالی است</string>
<string name="invalid_db_sig">نمی توانست فرمت پایگاه داده را تشخیص دهد.</string>
<string name="invalid_db_same_uuid">%1$s با همان UUID %2$s در حال حاضر وجود دارد.</string>
<string name="invalid_algorithm">الگوریتم اشتباه</string>
<string name="invalid_credentials">نمی توانست اعتبارنامه ها را بخواند.</string>
<string name="password">رمز عبور</string>
<string name="hint_pass">رمز عبور</string>
<string name="hint_length">طول</string>
<string name="hint_keyfile">Keyfile</string>
<string name="hint_group_name">نام گروه</string>
<string name="hint_generated_password">رمز عبور تولید شده</string>
<string name="hint_conf_pass">تایید رمز عبور</string>
<string name="generate_password">تولید رمز عبور</string>
<string name="file_browser">مدیر پرونده</string>
<string name="file_not_found_content">نمی توانست پرونده را پیدا کند. سعی کنید آن را از مرورگر فایل خود بازگشایی کنید.</string>
<string name="field_value">مقدار میدان</string>
<string name="field_name">نام زمینه</string>
<string name="error_string_type">این متن با مورد درخواست شده مطابقت ندارد.</string>
<string name="error_otp_digits">نشانه باید شامل %1$d تا %2$d رقم باشد.</string>
<string name="error_otp_period">دوره باید بین %1$d و %2$d ثانیه باشد.</string>
<string name="error_otp_counter">شمارنده باید بین %1$d و %2$d باشد.</string>
<string name="error_otp_secret_key">کلید راز باید در فرمت Base32 باشد.</string>
<string name="error_save_database">نمی توانست پایگاه داده را ذخیره کند.</string>
<string name="error_create_database_file">قادر به ایجاد پایگاه داده با این رمز عبور و keyfile نیست.</string>
<string name="error_create_database">قادر به ایجاد فایل پایگاه داده نیست.</string>
<string name="error_copy_group_here">شما نمی توانید یک گروه را در اینجا کپی کنید.</string>
<string name="error_copy_entry_here">شما نمی توانید یک ورودی را در اینجا کپی کنید.</string>
<string name="error_move_entry_here">شما نمی توانید یک ورودی را به اینجا منتقل کنید.</string>
<string name="error_move_folder_in_itself">شما نمی توانید یک گروه را به خود منتقل کنید.</string>
<string name="error_autofill_enable_service">قادر به فعال کردن سرویس پر کردن خودکار نبود.</string>
<string name="error_wrong_length">یک عدد کامل مثبت را در زمینه \"طول\" وارد کنید.</string>
<string name="error_label_exists">این برچسب در حال حاضر وجود دارد.</string>
<string name="error_string_key">هر رشته باید یک نام فیلد داشته باشد.</string>
<string name="error_rounds_too_large">\"دور تحول\" بیش از حد بالا است. تنظیم به 2147483648.</string>
<string name="error_pass_match">رمزهای عبور با هم مطابقت نمی کنند.</string>
<string name="error_disallow_no_credentials">حداقل یک اعتبار نامه باید تعیین شود.</string>
<string name="error_pass_gen_type">حداقل یک نوع تولید رمز عبور باید انتخاب شود</string>
<string name="error_load_database_KDF_memory">نميتونستم کليد رو بار کنم سعی کنید KDF \"استفاده از حافظه\" را پایین بیاورید.</string>
<string name="error_load_database">پایگاه داده شما را نمی توان بارگذاری کرد</string>
<string name="error_out_of_memory">هیچ حافظه ای برای بارگذاری کل پایگاه داده خود را.</string>
<string name="error_nokeyfile">یک فایل کلید را انتخاب کنید.</string>
<string name="error_no_name">نامی را وارد کنید.</string>
<string name="error_invalid_OTP">راز OTP نامعتبر.</string>
<string name="error_invalid_path">مطمئن شوید که مسیر درست است</string>
<string name="error_invalid_db">نمی توانست پایگاه داده را بخواند.</string>
<string name="error_file_not_create">نمی تواند پرونده ایجاد کند:</string>
<string name="error_can_not_handle_uri">نمی تواند این URI در KeePassDX رسیدگی کند.</string>
<string name="error_arc4">رمز جریان Arcfour پشتیبانی نمی شود</string>
<string name="entry_user_name">نام کاربری</string>
<string name="entry_url">آدرس</string>
<string name="entry_otp">otp</string>
<string name="otp_algorithm">الگوریتم</string>
<string name="otp_digits">رقم</string>
<string name="otp_counter">شمارنده</string>
<string name="otp_period">دوره (ثانیه)</string>
<string name="otp_secret">مخفی</string>
<string name="otp_type">نوع OTP</string>
<string name="entry_setup_otp">گذرواژه یکبار مصرف تنظیم کنید</string>
<string name="entry_title">عنوان</string>
<string name="entry_save">ذخیره</string>
<string name="entry_password">رمز عبور</string>
<string name="entry_not_found">نمی توانست داده های ورودی را پیدا کند.</string>
<string name="entry_modified">تغییر</string>
<string name="entry_keyfile">پرونده کلید</string>
<string name="entry_attachments">پیوست</string>
<string name="entry_history">تاریخچه</string>
<string name="entry_UUID">UUID</string>
<string name="entry_expires">منقضی</string>
<string name="entry_created">ایجاد</string>
<string name="entry_confpassword">تایید رمز عبور</string>
<string name="entry_notes">یادداشت</string>
<string name="entry_cancel">لغو</string>
<string name="entry_accessed">دیده</string>
<string name="html_about_contribution">به منظور &lt;strong&gt;نگاه آزادی ما&lt;/strong&gt; ، &lt;strong&gt;نقش های فیکسی&lt;/strong&gt; ، &lt;strong&gt;خدمات مضاعف&lt;/strong&gt; و &lt;strong&gt; همیشه فعال باشد&lt;/strong&gt; ، ما در &lt;strong&gt;مسابق&lt;/strong&gt; حساب می &lt;/strong&gt;.</string>
<string name="html_about_licence">KeePassDX © ٪1 $d Kunzisoft است &lt;strong&gt;منا منبع باز&lt;/strong&gt; و &lt;strong&gt;با تبلیغات&lt;/strong&gt;.
\nاین است که ارائه شده است، تحت &lt;strong&gt;GPLv3&lt;/strong&gt;، بدون هیچ گونه گارانتی.</string>
<string name="digits">رقم</string>
<string name="default_checkbox">استفاده به عنوان پایگاه داده پیش فرض</string>
<string name="decrypting_db">رمزگشایی محتوای پایگاه داده…</string>
<string name="database">پایگاه داده</string>
<string name="retrieving_db_key">بازیابی کلید پایگاه داده…</string>
<string name="select_to_copy">انتخاب برای کپی %1$s به کلیپ بورد</string>
<string name="content_description_keyboard_close_fields">زمینه های نزدیک</string>
<string name="content_description_remove_from_list">حذف</string>
<string name="content_description_update_from_list">روز رسانی</string>
<string name="content_description_remove_field">حذف فیلد</string>
<string name="entry_add_attachment">افزودن پیوست</string>
<string name="entry_add_field">اضافه کردن زمینه</string>
<string name="content_description_password_length">طول رمز عبور</string>
<string name="entry_password_generator">ژنراتور رمز عبور</string>
<string name="discard">دور انداختن</string>
<string name="discard_changes">تغییرات را دور بیندازید؟</string>
<string name="validate">اعتبار</string>
<string name="content_description_entry_icon">نماد ورود</string>
<string name="content_description_repeat_toggle_password_visibility">تکرار تغییر دید رمز عبور</string>
<string name="content_description_keyfile_checkbox">جعبه چک فایل کلید</string>
<string name="content_description_password_checkbox">جعبه چک رمز عبور</string>
<string name="content_description_file_information">اطلاعات فایل</string>
<string name="content_description_add_item">افزودن آیتم</string>
<string name="content_description_add_group">اضافه کردن گروه</string>
<string name="content_description_add_entry">افزودن ورودی</string>
<string name="content_description_add_node">اضافه کردن گره</string>
<string name="content_description_node_children">گره کودکان</string>
<string name="content_description_open_file">باز کردن فایل</string>
<string name="content_description_background">پس زمینه</string>
<string name="clipboard_timeout_summary">مدت زمان ذخیره سازی در کلیپ بورد (در صورت پشتیبانی توسط دستگاه شما)</string>
<string name="clipboard_timeout">تایم آوت کلیپ بورد</string>
<string name="clipboard_error_clear">نمی توانست کلیپ بورد را پاک کند</string>
<string name="clipboard_error">برخی از دستگاه ها اجازه نمی دهند برنامه ها از کلیپ بورد استفاده کنند.</string>
<string name="clipboard_error_title">خطای کلیپ بورد</string>
<string name="clipboard_cleared">کلیپ بورد پاک شد</string>
<string name="allow">اجازه</string>
<string name="file_manager_install_description">یک مدیر پرونده که عمل Intent را می پذیرد ACTION_CREATE_DOCUMENT و ACTION_OPEN_DOCUMENT برای ایجاد، باز کردن و ذخیره فایل های پایگاه داده مورد نیاز است.</string>
<string name="extended_ASCII">گسترش ASCII</string>
<string name="brackets">براکت</string>
<string name="application">برنامه</string>
<string name="app_timeout_summary">زمان بیکار قبل از قفل کردن پایگاه داده</string>
<string name="app_timeout">تایم آوت برنامه</string>
<string name="key_derivation_function">تابع مشتق کلید</string>
<string name="encryption_algorithm">الگوریتم رمزنگاری</string>
<string name="encryption">رمزگذاری</string>
<string name="security">امنیت</string>
<string name="master_key">کلید استاد</string>
<string name="add_group">اضافه کردن گروه</string>
<string name="edit_entry">ویرایش ورودی</string>
<string name="add_entry">افزودن ورودی</string>
<string name="accept">قبول</string>
<string name="about_description">پیاده سازی آندروید از مدیر رمز عبور KeePass</string>
<string name="homepage">صفحه اصلی</string>
<string name="feedback">بازخورد</string>
<string name="contribution">سهم</string>
<string name="contact">مخاطب</string>
<string name="filter">فیلتر</string>
</resources>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Käyttäjänimi</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_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_path">Varmista että polku on oikein.</string>
<string name="error_no_name">Anna nimi.</string>
@@ -245,8 +245,6 @@
<string name="monospace_font_fields_enable_title">Kenttäfontti</string>
<string name="recycle_bin_summary">Siirrä ryhmät ja tietueet \"Roskakori\" ryhmään ennen poistamista</string>
<string name="recycle_bin_title">Roskakorin käyttö</string>
<string name="full_file_path_enable_summary">Katso koko tiedostopolku</string>
<string name="full_file_path_enable_title">Tiedostopolku</string>
<string name="assign_master_key">Aseta pääavain</string>
<string name="path">Polku</string>
<string name="file_name">Tiedostonimi</string>

View File

@@ -65,7 +65,7 @@
<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_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_path">Vérifier la validité du chemin daccès.</string>
<string name="error_no_name">Saisir un nom.</string>
@@ -201,8 +201,6 @@
<string name="path">Chemin daccès</string>
<string name="assign_master_key">Affecter une clé principale</string>
<string name="create_keepass_file">Créer une nouvelle base de données</string>
<string name="full_file_path_enable_title">Chemin daccès du fichier</string>
<string name="full_file_path_enable_summary">Affiche le chemin daccès complet du fichier</string>
<string name="recycle_bin_title">Utilisation de la corbeille</string>
<string name="recycle_bin_summary">Déplace les groupes et les entrées dans le groupe «Corbeille» avant leur suppression</string>
<string name="monospace_font_fields_enable_title">Fonte de caractères des champs</string>
@@ -435,7 +433,7 @@
<string name="database_custom_color_title">Couleur de la base de données</string>
<string name="compression">Compression</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="error_save_database">Impossible denregistrer la base de données.</string>
<string name="menu_save_database">Enregistrer la base de données</string>
@@ -456,7 +454,7 @@
<string name="download_initialization">Initialisation…</string>
<string name="download_progression">En cours : %1$d%%</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_summary">Les entrées expirées sont cachées</string>
<string name="contact">Contact</string>

View File

@@ -62,7 +62,7 @@
<string name="entry_url">यू.आर.एल</string>
<string name="entry_user_name">उपयोगकर्ता का नाम</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_path">सुनिश्चित करें कि रास्ता सही है।</string>
<string name="error_no_name">एक नाम दर्ज करें।</string>

View File

@@ -205,8 +205,6 @@
<string name="file_name">Ime datoteke</string>
<string name="path">Putanja</string>
<string name="assign_master_key">Zadaj glavni ključ</string>
<string name="full_file_path_enable_title">Putanja datoteke</string>
<string name="full_file_path_enable_summary">Prikaži punu putanju do datoteke</string>
<string name="database_data_compression_title">Komprimiranje podataka</string>
<string name="database_data_compression_summary">Komprimiranje podataka smanjuje veličinu baze podataka.</string>
<string name="max_history_items_title">Maksimalni broj</string>
@@ -233,12 +231,12 @@
<string name="other">Ostalo</string>
<string name="compression">Komprimiranje</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="content_description_node_children">Pod-čvor</string>
<string name="entry_accessed">Pristupljeno</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_path">Provjeri putanju do datoteke.</string>
<string name="error_invalid_OTP">Neispravan OTP tajni ključ.</string>
@@ -290,7 +288,7 @@
<string name="password_size_title">Duljina generirane lozinke</string>
<string name="password_size_summary">Postavlja standardnu duljinu generiranih lozinki</string>
<string name="clipboard_explanation_summary">Kopiraj polja unosa koristeći međuspremnik tvog uređaja</string>
<string name="clipboard_notifications_summary">Aktiviraj obavijesti međuspremnika za kopiranje polja prilikom prikaza unosa</string>
<string name="clipboard_notifications_summary">Pokaži obavijesti međuspremnika za kopiranje polja prilikom prikaza unosa</string>
<string name="lock">Zaključavanje</string>
<string name="lock_database_screen_off_title">Zaključavanje ekrana</string>
<string name="recycle_bin_title">Koristi koš za smeće</string>
@@ -323,7 +321,7 @@
<string name="keyboard_key_vibrate_title">Vibracija tipki</string>
<string name="keyboard_key_sound_title">Zvuk tipki</string>
<string name="allow_no_password_title">Dozvoli bez lozinke</string>
<string name="allow_no_password_summary">Aktiviraj gumb „Otvori”, ako nijedna akreditacija nije odabrana</string>
<string name="allow_no_password_summary">Dozvoljava dodir gumba „Otvori”, ako nijedna akreditacija nije odabrana</string>
<string name="delete_entered_password_title">Izbriši lozinku</string>
<string name="delete_entered_password_summary">Briše upisanu lozinku nakon pokušaja povezivanja s bazom podataka</string>
<string name="enable_read_only_title">Zaštićeno od pisanja</string>
@@ -339,7 +337,7 @@
<string name="education_create_database_summary">Stvori svoju prvu datoteku za upravljanje lozinkama.</string>
<string name="education_select_database_title">Otvori jednu postojeću bazu podataka</string>
<string name="education_select_database_summary">Za daljnju upotrebu prijašnje datoteke baze podataka, otvori je iz upravitelja datoteka.</string>
<string name="remember_database_locations_title">Spremi mjesto baze podataka</string>
<string name="remember_database_locations_title">Zapamti mjesto baze podataka</string>
<string name="auto_focus_search_title">Brza pretraga</string>
<string name="error_create_database">Nije moguće stvoriti datoteku baze podataka.</string>
<string name="error_rounds_too_large">Previše „transformacijskih prolaza”. Postavlja se na 2147483648.</string>
@@ -350,8 +348,8 @@
<string name="discard_changes">Odbaciti promjene\?</string>
<string name="contact">Kontakt</string>
<string name="homepage">Početna stranica</string>
<string name="remember_keyfile_locations_title">Spremi mjesto datoteke ključa</string>
<string name="unavailable_feature_version">Tvoja Android verzija %1$s ne odgovara minimalno potrebnoj verziji %2$s.</string>
<string name="remember_keyfile_locations_title">Zapamti mjesto datoteke ključa</string>
<string name="unavailable_feature_version">Uređaj koristi Android verziju %1$s, ali potrebna je verzija %2$s ili novija.</string>
<string name="autofill_auto_search_summary">Automatski predloži rezultate pretrage od web domene ili ID-a aplikacije</string>
<string name="hide_broken_locations_summary">Sakrij pokvarene poveznice u popisu nedavnih baza podataka</string>
<string name="html_text_dev_feature">Ova se funkcija nalazi &lt;strong&gt;u razvoju&lt;/strong&gt; i treba tvoj &lt;strong&gt;doprinos&lt;/strong&gt; kako bi uskoro bila dostupna.</string>
@@ -377,7 +375,7 @@
<string name="keyboard_entry_timeout_summary">Istek vremena za brisanje unosa tipkovnicom</string>
<string name="education_read_only_title">Zaštiti bazu podataka od pisanja</string>
<string name="autofill_web_domain_blocklist_title">Popis blokiranja web domena</string>
<string name="education_biometric_title">Otključaj bazu podataka pomoću biometrije</string>
<string name="education_biometric_title">Biometrijsko otključavanje baze podataka</string>
<string name="kdf_AES">AES</string>
<string name="contribution">Doprinos</string>
<string name="open_biometric_prompt_store_credential">Za spremanje akreditacija, otvori biometrijsku prijavu</string>
@@ -397,7 +395,7 @@
<string name="keyboard_entry_timeout_title">Istek vremena</string>
<string name="auto_focus_search_summary">Pokreni pretragu prilikom otvaranja baze podataka</string>
<string name="education_entry_edit_summary">Uredi svoj unos pomoću prilagođenih polja. Moguće je unakrsno pozivanje podataka između različitih polja unosa.</string>
<string name="remember_database_locations_summary">Zapamti mjesto baza podataka</string>
<string name="remember_database_locations_summary">Pamti mjesto spremanja baza podataka</string>
<string name="education_field_copy_summary">Kopirana polja mogu se umetnuti bilo gdje.
\n
\nKoristi preferirani način ispunjavanja obrazaca.</string>
@@ -418,7 +416,7 @@
<string name="autofill_block">Blokiranje automatskog ispunjavanja</string>
<string name="keystore_not_accessible">Baza ključeva nije ispravno inicijalizirana.</string>
<string name="icon_pack_choose_summary">Paket ikona, koji se koristi u aplikaciji</string>
<string name="hide_expired_entries_summary">Istekli unosi su skrivaju</string>
<string name="hide_expired_entries_summary">Istekli unosi se ne pokazuju</string>
<string name="education_lock_title">Zaključaj bazu podataka</string>
<string name="open_biometric_prompt_unlock_database">Za otključavanje baze podataka, otvori biometrijsku prijavu</string>
<string name="education_unlock_title">Otključaj bazu podataka</string>
@@ -454,12 +452,12 @@
\nSpremi sigurnosnu kopiju datoteke baze podataka na sigurno mjesto nakon svake promjene.</string>
<string name="configure_biometric">Biometrijska prijava je podržana, ali nije postavljena.</string>
<string name="subdomain_search_title">Pretraživanje poddomenom</string>
<string name="education_setup_OTP_summary">Postavi upravljanje jednokratnih lozinki (HOTP / TOTP) za generiranje tokena koji je potreban za dvofaktorsku autentifikaciju (2FA).</string>
<string name="education_setup_OTP_summary">Postavi upravljanje jednokratnim lozinkama (HOTP / TOTP) za generiranje tokena koji je potreban za dvofaktorsku autentifikaciju (2FA).</string>
<string name="hide_expired_entries_title">Sakrij istekle unose</string>
<string name="download_finalization">Završavanje …</string>
<string name="download">Preuzimanje</string>
<string name="lock_database_show_button_summary">Prikazuje gumb za zaključavanje u korisničkom sučelju</string>
<string name="remember_keyfile_locations_summary">Zapamti mjesto datoteka ključeva baze podataka</string>
<string name="remember_keyfile_locations_summary">Pamti mjesto spremanja datoteka ključeva</string>
<string name="html_text_ad_free">Za razliku od mnogih aplikacija za upravljanje lozinkama, ova je &lt;strong&gt;bez oglasa&lt;/strong&gt;, &lt;strong&gt;copylefted slobodan softver&lt;/strong&gt; i ne prikuplja osobne podatke na svojim poslužiteljima, bez obzira na korištenu verziju.</string>
<string name="rounds">Transformacijski prolazi</string>
<string name="download_initialization">Inicijaliziranje …</string>
@@ -469,7 +467,7 @@
\n
\nGrupe (~mape) organiziraju unose u bazi podataka.</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_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>

View File

@@ -60,7 +60,7 @@
<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_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_path">Győződjön meg róla, hogy az útvonal helyes.</string>
<string name="error_no_name">Adjon meg egy nevet.</string>
@@ -235,8 +235,6 @@
<string name="file_name">Fájlnév</string>
<string name="path">Útvonal</string>
<string name="assign_master_key">Mesterkulcs hozzárendelése</string>
<string name="full_file_path_enable_title">Fájlútvonal</string>
<string name="full_file_path_enable_summary">A teljes fájlútvonal megtekintése</string>
<string name="recycle_bin_title">Kuka használata</string>
<string name="recycle_bin_summary">A csoportok és bejegyzések „Kukába” helyezése törlés előtt</string>
<string name="monospace_font_fields_enable_title">Mező betűkészlete</string>
@@ -384,7 +382,7 @@
<string name="contact">Kapcsolat</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="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_progression">Folyamatban: %1$d%%</string>
<string name="download_initialization">Előkészítés…</string>
@@ -405,7 +403,7 @@
<string name="menu_save_database">Adatbázis mentése</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="compression_gzip">gzip</string>
<string name="compression_gzip">Gzip</string>
<string name="compression_none">Nincs</string>
<string name="compression">Tömörítés</string>
<string name="database_custom_color_title">Egyéni adatbázisszín</string>

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_results">Pencarian Tidak Ditemukan</string>
<string name="never">Tak Pernah</string>
<string name="minus">Minimal</string>
<string name="menu_delete_entry_history">Hapus Riwayat</string>
<string name="menu_restore_entry_history">Pulihkan Riwayat</string>
<string name="menu_empty_recycle_bin">Kosongkan Tempat Sampah</string>
<string name="menu_open_file_read_and_write">Bisa Diubah</string>
<string name="menu_file_selection_read_only">Lindungi Dari Perubahan</string>
<string name="menu_url">Membuka Tautan</string>
<string name="menu_biometric_remove_key">Hapus Kunci Sidik Jari Yang Tersimpan</string>
<string name="menu_showpass">Tampilkan Kata Sandi</string>
<string name="menu_search">Cari</string>
<string name="menu_open">Buka</string>
<string name="menu_save_database">Simpan Basisdata</string>
<string name="menu_lock">Basisdata Terkunci</string>
<string name="menu_hide_password">Sembunyikan Kata Sandi</string>
<string name="menu_cancel">Batal</string>
<string name="menu_delete">Hapus</string>
<string name="menu_paste">Tempel</string>
<string name="menu_move">Pindah</string>
<string name="menu_copy">Salin</string>
<string name="menu_edit">Ubah</string>
<string name="menu_donate">Donasi</string>
<string name="menu_master_key_settings">Pengaturan Kunci Utama</string>
<string name="menu_security_settings">Pengaturan Keamanan</string>
<string name="menu_app_settings">Pengaturan Aplikasi</string>
<string name="menu_database_settings">Pengaturan Basisdata</string>
<string name="menu_advanced_unlock_settings">Buka Kunci Lanjutan</string>
<string name="menu_form_filling_settings">Pengisian Formulir</string>
<string name="settings">Pengaturan</string>
<string name="copy_field">Salinan dari %1$s</string>
<string name="menu_change_key_settings">Ubah Kunci Utama</string>
<string name="about">Tentang</string>
<string name="hide_password_summary">Secara otomatis tutupi kata sandi (***)</string>
<string name="hide_password_title">Sembunyikan Kata Sandi</string>
<string name="lowercase">Huruf Kecil</string>
<string name="loading_database">Memuat basisdata…</string>
<string name="creating_database">Pembuatan basisdata…</string>
<string name="list_size_summary">Ukuran teks dalam daftar elemen</string>
<string name="list_size_title">Ukuran daftar item</string>
<string name="list_groups_show_number_entries_summary">Tampilkan jumlah entri dalam sebuah grup</string>
<string name="list_groups_show_number_entries_title">Tampilkan jumlah entri</string>
<string name="list_entries_show_username_summary">Tampilkan nama pengguna dalam daftar entri</string>
<string name="list_entries_show_username_title">Tampilkan nama pengguna</string>
<string name="length">Panjangnya</string>
<string name="keyfile_is_empty">File Kunci kosong.</string>
<string name="invalid_db_sig">Tidak bisa mengenali format basisdata.</string>
<string name="invalid_db_same_uuid">%1$s dengan UUID yang sama %2$s sudah ada.</string>
<string name="invalid_algorithm">Algoritma salah.</string>
<string name="invalid_credentials">Tidak bisa membaca kredensial.</string>
<string name="password">Kata Sandi</string>
<string name="hint_pass">Kata Sandi</string>
<string name="hint_length">Panjangnya</string>
<string name="hint_keyfile">File Kunci</string>
<string name="hint_group_name">Nama Grup</string>
<string name="hint_generated_password">Kata Sandi Telah Dibuat</string>
<string name="hint_conf_pass">Konfirmasi Kata Sandi</string>
<string name="generate_password">Buatkan Kata Sandi</string>
<string name="file_browser">Pengelola File</string>
<string name="file_not_found_content">Tidak bisa menemukan file. Cobalah buka kembali dari pengelola file anda.</string>
<string name="field_value">Nilai Bidang</string>
<string name="field_name">Nama Bidang</string>
<string name="error_string_type">Teks ini tidak sesuai dengan item yang diminta.</string>
<string name="error_otp_digits">Token harus berisi %1$d sampai %2$d dijit.</string>
<string name="error_otp_period">Periode harus antara %1$d dan %2$d detik.</string>
<string name="error_otp_counter">Penghitung harus antara %1$d dan %2$d.</string>
<string name="error_otp_secret_key">Kunci rahasia harus dalam format Base32.</string>
<string name="error_save_database">Tidak bisa menyimpan basisdata.</string>
<string name="error_create_database_file">Tidak bisa membuat basisdata dengan kata sandi dan file kunci ini.</string>
<string name="error_create_database">Tidak bisa membuat file basisdata.</string>
<string name="error_copy_group_here">Anda tidak bisa menyalin grup di sini.</string>
<string name="error_copy_entry_here">Anda tidak bisa menyalin entri di sini.</string>
<string name="error_move_entry_here">Anda tidak bisa memindahkan entri ke sini.</string>
<string name="error_move_folder_in_itself">Anda tidak bisa memindahkan grup ke grup itu sendiri.</string>
<string name="error_autofill_enable_service">Tidak bisa mengaktifkan layanan IsiOtomatis.</string>
<string name="error_wrong_length">Masukkan bilangan bulat positif di bidang \"Panjang\".</string>
<string name="error_label_exists">Label ini sudah ada.</string>
<string name="error_string_key">Setiap string harus memiliki bidang nama.</string>
<string name="error_rounds_too_large">\"Putaran Transformasi\" terlalu tinggi. Atur ke 2147483648.</string>
<string name="error_pass_match">Kata sandi tidak sesuai.</string>
<string name="error_disallow_no_credentials">Setidaknya ada satu kredensial yang harus ditetapkan.</string>
<string name="error_pass_gen_type">Setidaknya ada satu jenis pembuatan kata sandi yang harus dipilih.</string>
<string name="error_load_database_KDF_memory">Tidak bisa memuat kunci. Cobalah untuk mengurangi penggunaan memori (mematikan aplikasi lainnya).</string>
<string name="error_load_database">Tidak bisa memuat basisdata anda.</string>
<string name="error_out_of_memory">Tidak cukup memori untuk memuat seluruh basisdata anda.</string>
<string name="entry_keyfile">File Kunci</string>
<string name="error_nokeyfile">Pilih file kunci.</string>
<string name="error_no_name">Ketik sebuah nama.</string>
<string name="error_invalid_OTP">Rahasia OTP tidak valid.</string>
<string name="error_invalid_path">Pastikan lokasi filenya sudah benar.</string>
<string name="error_invalid_db">Tidak bisa membaca basisdata.</string>
<string name="error_file_not_create">Tidak bisa membuat file:</string>
<string name="entry_add_attachment">Tambahkan Lampiran</string>
<string name="digits">Dijit</string>
<string name="app_timeout_summary">Waktu idle sebelum mengunci basisdata</string>
<string name="file_manager_install_description">Ijin Pengolaan File untuk menerima perintah \"ACTION_CREATE_DOCUMENT\" dan \"ACTION_OPEN_DOCUMENT\" diperlukan untuk membuat, membuka, dan menyimpan file basisdata.</string>
<string name="otp_type">Jenis OTP</string>
<string name="entry_setup_otp">Penyiapan Sandi Sekali Pakai (OTP)</string>
<string name="error_can_not_handle_uri">Tidak bisa menangani URI ini di KeePassDX.</string>
<string name="error_arc4">Stream Cipher Arcfour tidak didukung.</string>
<string name="entry_user_name">Nama Pengguna</string>
<string name="entry_url">Tautan</string>
<string name="entry_otp">OTP (One Time Password)</string>
<string name="otp_algorithm">Algoritma</string>
<string name="otp_digits">Dijit</string>
<string name="otp_counter">Penghitung</string>
<string name="otp_period">Periode (detik)</string>
<string name="otp_secret">Rahasia</string>
<string name="entry_title">Judul</string>
<string name="entry_save">Simpan</string>
<string name="entry_password">Kata Sandi</string>
<string name="entry_not_found">Tidak bisa menemukan data entri.</string>
<string name="retrieving_db_key">Mengambil kunci basisdata…</string>
<string name="clipboard_error_clear">Tidak bisa membersihkan papan klip</string>
<string name="clipboard_timeout_summary">Durasi simpan pada papan klip (jika didukung oleh perangkat anda)</string>
<string name="content_description_repeat_toggle_password_visibility">Ulangi Peralihan Penampakan Kata Sandi</string>
<string name="clipboard_timeout">Batas Waktu Papan Klip</string>
<string name="clipboard_cleared">Papan Klip Dibersihkan</string>
<string name="entry_modified">Diubah</string>
<string name="entry_attachments">Lampiran</string>
<string name="entry_history">Riwayat</string>
<string name="entry_UUID">UUID (Identitas Unik Universal)</string>
<string name="entry_expires">Kedaluwarsa</string>
<string name="entry_created">Dibuat</string>
<string name="entry_confpassword">Konfirmasi Kata Sandi</string>
<string name="entry_notes">Catatan</string>
<string name="entry_cancel">Batalkan</string>
<string name="entry_accessed">Diakses</string>
<string name="html_about_contribution">Untuk &lt;strong&gt;menjaga kebebasan kami&lt;/strong&gt;, &lt;strong&gt;memperbaiki bug&lt;/strong&gt;, &lt;strong&gt; menambah fitur&lt;/strong&gt; dan &lt;strong&gt;agar selalu aktif&lt;/strong&gt;, kami mengandalkan &lt;strong&gt; kontribusi&lt;/strong&gt;.</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft &lt;strong&gt; open source &lt;/strong&gt; dan &lt;strong&gt; tanpa iklan &lt;/strong&gt;.&lt;br&gt;Tersedia apa adanya, di bawah lisensi &lt;strong&gt; GPLv3 &lt;/strong&gt;, tanpa jaminan apa pun.</string>
<string name="default_checkbox">Gunakan sebagai basisdata standar</string>
<string name="decrypting_db">Mendekripsi konten basisdata…</string>
<string name="database">BasisData</string>
<string name="select_to_copy">Pilih untuk menyalin %1$s ke papan klip</string>
<string name="content_description_keyboard_close_fields">Tutup Bidang</string>
<string name="content_description_remove_from_list">Buang</string>
<string name="content_description_update_from_list">Perbarui</string>
<string name="content_description_remove_field">Hapus Bidang</string>
<string name="entry_add_field">Tambahkan Bidang</string>
<string name="content_description_password_length">Panjang Kata Sandi</string>
<string name="entry_password_generator">Pembuat Kata Sandi</string>
<string name="discard">Batalkan</string>
<string name="discard_changes">Batalkan perubahan\?</string>
<string name="validate">Mengesahkan</string>
<string name="content_description_entry_icon">Ikon Entri</string>
<string name="content_description_keyfile_checkbox">Kotak Centang Berkas Kunci</string>
<string name="content_description_password_checkbox">Kotak Centang Kata Sandi</string>
<string name="content_description_file_information">Info File</string>
<string name="content_description_add_item">Tambahkan Item</string>
<string name="content_description_add_group">Tambahkan Grup</string>
<string name="content_description_add_entry">Tambahkan Entri</string>
<string name="content_description_add_node">Tambahkan Node</string>
<string name="content_description_node_children">Node Anak</string>
<string name="content_description_open_file">Buka Berkas</string>
<string name="content_description_background">Latar Belakang</string>
<string name="clipboard_error">Beberapa perangkat tidak mengizinkan aplikasi menggunakan papan klip.</string>
<string name="clipboard_error_title">Kesalahan Papan Klip</string>
<string name="allow">Ijinkan</string>
<string name="extended_ASCII">ASCII Diperpanjang</string>
<string name="brackets">Tanda Kurung</string>
<string name="application">Aplikasi</string>
<string name="app_timeout">Batas Waktu Aplikasi</string>
<string name="key_derivation_function">Fungsi Derivasi Kunci</string>
<string name="encryption_algorithm">Algoritma Enkripsi</string>
<string name="encryption">Enkripsi</string>
<string name="security">Keamanan</string>
<string name="master_key">Kunci Utama</string>
<string name="add_group">Tambahkan Grup</string>
<string name="edit_entry">Rubah Entri</string>
<string name="add_entry">Tambahkan Entri</string>
<string name="accept">Terima</string>
<string name="about_description">Implementasi Android dari pengelola kata sandi KeePass</string>
<string name="homepage">Beranda</string>
<string name="feedback">Umpan Balik</string>
<string name="contribution">Kontribusi</string>
<string name="contact">Kontak</string>
</resources>

View File

@@ -37,7 +37,7 @@
<string name="clipboard_error">Alcuni dispositivi non permettono alle app di usare gli appunti.</string>
<string name="clipboard_error_clear">Eliminazione degli appunti fallita</string>
<string name="clipboard_timeout">Scadenza appunti</string>
<string name="clipboard_timeout_summary">Tempo prima di eliminare gli appunti</string>
<string name="clipboard_timeout_summary">Tempo prima di eliminare gli appunti (se supportato dal dispositivo)</string>
<string name="select_to_copy">Copia %1$s negli appunti</string>
<string name="retrieving_db_key">Creazione file chiave database…</string>
<string name="database">Banca dati</string>
@@ -63,7 +63,7 @@
<string name="entry_user_name">Nome utente</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_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_path">Assicurati che il percorso sia corretto.</string>
<string name="error_no_name">Inserisci un nome.</string>
@@ -228,8 +228,6 @@
<string name="path">Percorso</string>
<string name="assign_master_key">Assegna una chiave master</string>
<string name="create_keepass_file">Crea un nuovo database</string>
<string name="full_file_path_enable_title">Percorso file</string>
<string name="full_file_path_enable_summary">Visualizza il percorso file completo</string>
<string name="recycle_bin_title">Uso del Cestino</string>
<string name="recycle_bin_summary">Sposta i gruppi e le voci nel gruppo \"Cestino\" prima di eliminarlo</string>
<string name="monospace_font_fields_enable_title">Carattere campi</string>
@@ -343,7 +341,7 @@
<string name="lock_database_back_root_title">Premere \'\'Indietro\'\' per bloccare</string>
<string name="lock_database_back_root_summary">Bloccare il database quando l\'utente preme il pulsante Indietro nella schermata principale</string>
<string name="clear_clipboard_notification_title">Pulisci alla chiusura</string>
<string name="clear_clipboard_notification_summary">Blocca il database alla chiusura della notifica</string>
<string name="clear_clipboard_notification_summary">Blocca il database quando scade la durata degli appunti o la notifica viene chiusa dopo che inizi ad usarlo</string>
<string name="recycle_bin">Cestino</string>
<string name="keyboard_selection_entry_title">Selezione elemento</string>
<string name="keyboard_selection_entry_summary">Mostra i campi di input nella Magitastiera durante la visualizzazione di un elemento</string>
@@ -448,7 +446,7 @@
<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="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_progression">Avanzamento %1$d%%</string>
<string name="download_initialization">Inizializzazione…</string>
@@ -456,12 +454,12 @@
<string name="education_setup_OTP_title">Imposta OTP</string>
<string name="enable_auto_save_database_summary">Salva il database dopo ogni azione importante (in modalità \"Modificabile\")</string>
<string name="enable_auto_save_database_title">Salvataggio automatico del database</string>
<string name="autofill_auto_search_summary">Suggerisci automaticamente risultati dal dominio web o l\'ID dell\'applicazione</string>
<string name="autofill_auto_search_summary">Suggerisci automaticamente risultati dal dominio web o ID dell\'applicazione</string>
<string name="autofill_auto_search_title">Ricerca automatica</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="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">Compressione</string>
<string name="database_custom_color_title">Colore del database customizzato</string>
@@ -490,4 +488,9 @@
<string name="subdomain_search_summary">Cerca nei domini web includendo i sotto-domini</string>
<string name="subdomain_search_title">Ricerca per sotto-dominio</string>
<string name="content_description_add_item">Aggiungi elemento</string>
<string name="keyboard_previous_fill_in_summary">Torna automaticamente alla tastiera precedente quando si esegue l\'azione del tasto automatico</string>
<string name="keyboard_previous_fill_in_title">Azione tasto automatico</string>
<string name="keyboard_previous_database_credentials_summary">Torna automaticamente alla tastiera precedente nella schermata delle credenziali del database</string>
<string name="keyboard_previous_database_credentials_title">Schermata credenziali database</string>
<string name="keyboard_change">Cambia tastiera</string>
</resources>

View File

@@ -60,7 +60,7 @@
<string name="entry_user_name">שם משתמש</string>
<string name="error_arc4">צופן זרם Arcfour אינו נתמך.</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_path">נתיב לא חוקי.</string>
<string name="error_no_name">שם נדרש.</string>

View File

@@ -25,11 +25,11 @@
<string name="add_group">グループを追加</string>
<string name="encryption_algorithm">暗号化アルゴリズム</string>
<string name="app_timeout">アプリのタイムアウト</string>
<string name="app_timeout_summary">アプリがアイドル状態になってからデータベースをロックするまでの時間</string>
<string name="app_timeout_summary">この期間アプリの操作がなかった場合、データベースをロックしま</string>
<string name="application">アプリ</string>
<string name="menu_app_settings">アプリの設定</string>
<string name="brackets">かっこ</string>
<string name="file_manager_install_description">ACTION_CREATE_DOCUMENT と ACTION_OPEN_DOCUMENT のインテント アクションを受け入れるファイル マネージャーがデータベース ファイルの作成、オープン、保存に必要です。</string>
<string name="file_manager_install_description">ACTION_CREATE_DOCUMENT と ACTION_OPEN_DOCUMENT のインテント アクションを受け入れるファイル マネージャーがデータベース ファイルの作成、オープン、保存に必要です。</string>
<string name="clipboard_cleared">クリップボードを消去しました</string>
<string name="clipboard_timeout">クリップボードのタイムアウト</string>
<string name="clipboard_timeout_summary">クリップボード内での保存期間(デバイスが対応している場合)</string>
@@ -39,7 +39,7 @@
<string name="decrypting_db">データベースの内容を復号しています…</string>
<string name="default_checkbox">デフォルトのデータベースとして使用</string>
<string name="digits">数字</string>
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft は&lt;strong&gt;オープンソース&lt;/strong&gt;&lt;strong&gt;広告はありません&lt;/strong&gt;
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft は&lt;strong&gt;オープンソース&lt;/strong&gt;&lt;strong&gt;広告なし&lt;/strong&gt;です
\nそのままの状態で、&lt;strong&gt;GPLv3&lt;/strong&gt; ライセンスの下、いかなる保証もなく提供されます。</string>
<string name="select_database_file">既存のデータベースを開く</string>
<string name="entry_accessed">アクセス日時</string>
@@ -57,7 +57,7 @@
<string name="entry_user_name">ユーザー名</string>
<string name="error_arc4">Arcfour ストリーム暗号には対応していません。</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_path">パスが正しいことを確認してください。</string>
<string name="error_no_name">名前を入力してください。</string>
@@ -65,7 +65,7 @@
<string name="error_out_of_memory">データベース全体を読み込むメモリがありません。</string>
<string name="error_pass_gen_type">少なくとも 1 つのパスワード生成タイプを選択する必要があります。</string>
<string name="error_pass_match">パスワードが一致しません。</string>
<string name="error_rounds_too_large">「変換ラウンド」が多ぎます。2147483648 に設定します。</string>
<string name="error_rounds_too_large">「変換ラウンド」が多ぎます。2147483648 に設定します。</string>
<string name="error_wrong_length">「長さ」フィールドには正の整数を入力してください。</string>
<string name="file_browser">ファイル マネージャー</string>
<string name="generate_password">パスワードを生成</string>
@@ -118,13 +118,13 @@
<string name="special">特殊文字</string>
<string name="search">検索</string>
<string name="encryption_twofish">Twofish</string>
<string name="underline">下線</string>
<string name="underline">アンダースコア</string>
<string name="unsupported_db_version">対応していないバージョンのデータベースです。</string>
<string name="uppercase">大文字</string>
<string name="version_label">バージョン %1$s</string>
<string name="education_unlock_summary">データベースのロックを解除するには、パスワードまたはキーファイル、またはその両方を入力します。
\n
\nデータベース ファイルは変更するたびに安全な場所バックアップしてください。</string>
\nデータベース ファイルは変更するたびに安全な場所バックアップしてください。</string>
<string-array name="timeout_options">
<item>5秒</item>
<item>10秒</item>
@@ -168,7 +168,7 @@
<string name="menu_move">移動</string>
<string name="menu_paste">貼り付け</string>
<string name="menu_cancel">キャンセル</string>
<string name="menu_biometric_remove_key">保存済み生体認証キーを削除</string>
<string name="menu_biometric_remove_key">保存済み生体を削除</string>
<string name="menu_file_selection_read_only">書き込み禁止</string>
<string name="menu_open_file_read_and_write">変更可能</string>
<string name="create_keepass_file">新しいデータベースを作成</string>
@@ -190,7 +190,7 @@
<string name="content_description_add_group">グループを追加</string>
<string name="content_description_file_information">ファイル情報</string>
<string name="content_description_entry_icon">エントリーのアイコン</string>
<string name="entry_password_generator">パスワード ジェネレータ</string>
<string name="entry_password_generator">パスワード生成機能</string>
<string name="content_description_password_length">パスワードの長さ</string>
<string name="entry_add_field">フィールドを追加</string>
<string name="content_description_remove_field">フィールドを削除</string>
@@ -199,12 +199,12 @@
<string name="configure_biometric">生体認証プロンプト対応端末ですが未設定です。</string>
<string name="master_key">マスターキー</string>
<string name="entry_history">履歴</string>
<string name="otp_type">OTP の型式</string>
<string name="otp_type">OTP の種類</string>
<string name="otp_period">周期(秒)</string>
<string name="otp_algorithm">アルゴリズム</string>
<string name="entry_otp">OTP</string>
<string name="error_copy_group_here">ここではグループをコピーすることはできません。</string>
<string name="error_otp_secret_key">秘密鍵は Base32 形式内でなければなりません。</string>
<string name="error_otp_secret_key">シークレット キーは Base32 形式内でなければなりません。</string>
<string name="error_otp_period">周期は %1$d 秒から %2$d 秒の間でなければなりません。</string>
<string name="creating_database">データベースを作成しています…</string>
<string name="menu_security_settings">セキュリティの設定</string>
@@ -225,7 +225,7 @@
<string name="error_create_database">データベース ファイルを作成できません。</string>
<string name="error_label_exists">このラベルはすでに存在します。</string>
<string name="error_disallow_no_credentials">少なくとも 1 つの認証情報を設定する必要があります。</string>
<string name="error_invalid_OTP">OTP 秘密鍵が無効です。</string>
<string name="error_invalid_OTP">無効な OTP シークレットです。</string>
<string name="otp_digits"></string>
<string name="entry_setup_otp">ワンタイム パスワードを設定</string>
<string name="entry_attachments">添付ファイル</string>
@@ -244,7 +244,7 @@
<string name="open_biometric_prompt_store_credential">生体認証プロンプトを開き認証情報を保存します</string>
<string name="open_biometric_prompt_unlock_database">生体認証プロンプトを開きロックを解除します</string>
<string name="keystore_not_accessible">キーストアが正しく初期化されていません。</string>
<string name="warning_no_encryption_key">暗号化キーなしで続行しますか?</string>
<string name="warning_no_encryption_key">暗号なしで続行しますか?</string>
<string name="warning_empty_password">パスワードによるロック解除の保護なしで続行しますか?</string>
<string name="warning_database_read_only">データベースの変更を保存するために、ファイル書き込みアクセスを許可します</string>
<string name="search_results">検索結果</string>
@@ -259,10 +259,10 @@
<string name="hide_broken_locations_summary">最近使ったデータベースの一覧で、壊れたリンクを非表示にします</string>
<string name="hide_broken_locations_title">データベースへの壊れたリンクを非表示にする</string>
<string name="show_recent_files_summary">最近使ったデータベースの場所を表示します</string>
<string name="remember_keyfile_locations_summary">データベースのキーファイルの場所を記憶します</string>
<string name="remember_keyfile_locations_title">キーファイルの場所を保存</string>
<string name="remember_database_locations_summary">データベースの場所を記憶します</string>
<string name="remember_database_locations_title">データベースの場所を保存</string>
<string name="remember_keyfile_locations_summary">キーファイルの保存先を追跡します</string>
<string name="remember_keyfile_locations_title">キーファイルの場所を記憶</string>
<string name="remember_database_locations_summary">データベースの保存先を追跡します</string>
<string name="remember_database_locations_title">データベースの場所を記憶</string>
<string name="selection_mode">選択モード</string>
<string name="contains_duplicate_uuid_procedure">重複したエントリーに対する新しい UUID を生成して、問題を解決し続行しますか?</string>
<string name="contains_duplicate_uuid">データベースには重複する UUID が含まれています。</string>
@@ -272,7 +272,7 @@
<string name="error_string_type">このテキストは指定された項目と整合しません。</string>
<string name="error_otp_counter">カウンターは %1$d から %2$d の間でなければなりません。</string>
<string name="otp_counter">カウンター</string>
<string name="otp_secret">秘密鍵</string>
<string name="otp_secret">シークレット</string>
<string name="content_description_keyboard_close_fields">フィールドを閉じる</string>
<string name="content_description_update_from_list">更新</string>
<string name="entry_add_attachment">添付ファイルを追加</string>
@@ -287,21 +287,19 @@
<string name="education_generate_password_summary">エントリーに関連付ける強力なパスワードを生成します。フォームの基準に従って定義することは簡単で、安全なパスワードを忘れることはありません。</string>
<string name="monospace_font_fields_enable_summary">フィールド内で使用するフォントを変更して、文字を見やすくします</string>
<string name="monospace_font_fields_enable_title">フィールド フォント</string>
<string name="full_file_path_enable_summary">ファイルのフルパスを表示します</string>
<string name="full_file_path_enable_title">ファイルパス</string>
<string name="path">パス</string>
<string name="education_generate_password_title">強力なパスワードを作成</string>
<string name="biometric_auto_open_prompt_summary">データベースが生体認証を使用するように設定されている場合、生体情報の取得を自動的に求めます</string>
<string name="biometric_delete_all_key_title">暗号化キーを削除</string>
<string name="biometric_delete_all_key_title">暗号を削除</string>
<string name="biometric_auto_open_prompt_title">生体認証プロンプトを自動で開く</string>
<string name="biometric_delete_all_key_warning">生体認証に関するすべての暗号化キーを削除しますか?</string>
<string name="biometric_delete_all_key_summary">生体認証に関するすべての暗号化キーを削除します</string>
<string name="biometric_delete_all_key_warning">生体認証に関するすべての暗号を削除しますか?</string>
<string name="biometric_delete_all_key_summary">生体認証に関するすべての暗号を削除します</string>
<string name="advanced_unlock_explanation_summary">高度なロック解除を使用すると、データベースがより簡単に開きます</string>
<string name="education_lock_summary">データベースをすばやくロックします。時間が経ったり画面がオフになったときロックするようアプリを設定することもできます。</string>
<string name="education_lock_summary">データベースをすばやくロックします。時間が経ったり画面がオフになったときロックするようアプリを設定することもできます。</string>
<string name="education_lock_title">データベースをロック</string>
<string name="education_sort_summary">エントリーとグループの並べ替え方法を選択します。</string>
<string name="education_search_summary">パスワードを取得するには、タイトル、ユーザー名、または他のフィールドの内容を入力します。</string>
<string name="education_new_node_summary">エントリーはデジタル アイデンティティの管理に役立ちます。
<string name="education_new_node_summary">エントリーはデジタル ID の管理に役立ちます。
\n
\nグループ≒フォルダはデータベース内のエントリーを整理します。</string>
<string name="education_search_title">エントリーを検索</string>
@@ -313,14 +311,14 @@
<string name="education_select_database_title">既存のデータペースを開く</string>
<string name="list_password_generator_options_title">パスワードの文字種</string>
<string name="build_label">Build %1$s</string>
<string name="warning_password_encoding">データベース ファイル内のテキスト エンコーディング形式以外の文字を、パスワードに用いることは避けてください(認識されない文字は同じ文字に変換されます)。</string>
<string name="warning_password_encoding">データベース ファイル内のテキスト エンコーディング形式以外の文字を、パスワードに使うことは避けてください(認識されない文字は同じ文字に変換されます)。</string>
<string name="sort_groups_before">グループを先に並べる</string>
<string name="enable_auto_save_database_title">データベースを自動保存</string>
<string name="enable_read_only_summary">デフォルトではデータベースを読み取り専用で開きます</string>
<string name="enable_read_only_title">書き込み禁止</string>
<string name="delete_entered_password_summary">入力されたパスワードをデータベースへの接続試行後に削除します</string>
<string name="delete_entered_password_title">パスワードを削除</string>
<string name="allow_no_password_summary">認証情報が選択されていない場合でも「開く」ボタンを有効にします</string>
<string name="allow_no_password_summary">認証情報が選択されていない場合でも「開く」ボタンのタップを許可します</string>
<string name="autofill_block_restart">ブロッキングを有効にするには、そのフォームを含むアプリを再起動します。</string>
<string name="autofill_block">自動入力をブロック</string>
<string name="autofill_web_domain_blocklist_title">ウェブドメインのブロックリスト</string>
@@ -343,7 +341,7 @@
<string name="lock_database_screen_off_title">画面ロック</string>
<string name="lock">ロック</string>
<string name="clipboard_warning">クリップボードの自動削除に失敗した場合は、手動でその履歴を削除してください。</string>
<string name="clipboard_notifications_summary">エントリーを表示しているとき、フィールドをコピーするクリップボード通知を有効にします</string>
<string name="clipboard_notifications_summary">エントリーを開いているとき、フィールドをコピーするクリップボード通知を表示します</string>
<string name="clipboard_notifications_title">クリップボード通知</string>
<string name="clipboard">クリップボード</string>
<string name="database_opened">データベースが開かれています</string>
@@ -363,9 +361,9 @@
<string name="no_credentials_stored">データベースの保存済み認証情報はありません。</string>
<string name="biometric_scanning_error">生体認証エラー:%1$s</string>
<string name="biometric_not_recognized">生体情報を認識できませんでした</string>
<string name="biometric_invalid_key">生体認証キーが読み取れません。削除して生体認証の手順を繰り返してください。</string>
<string name="biometric_invalid_key">生体が読み取れません。削除して生体認証の手順を繰り返してください。</string>
<string name="encrypted_value_stored">保存された暗号化済みパスワード</string>
<string name="biometric_prompt_extract_credential_message">生体情報を用いてデータベースの認証情報を取り出します</string>
<string name="biometric_prompt_extract_credential_message">生体情報を使ってデータベースの認証情報を取り出します</string>
<string name="html_text_feature_generosity">この&lt;strong&gt;ビジュアル スタイル&lt;/strong&gt;はあなたの厚意により利用可能となります。</string>
<string name="subdomain_search_summary">サブドメインの制約つきでウェブドメインを検索します</string>
<string name="lock_database_back_root_summary">ユーザーがルート画面上で戻るボタンをタップしたとき、データベースをロックします</string>
@@ -380,7 +378,7 @@
<string name="education_biometric_summary">スキャンした生体情報にパスワードをリンクして、データベースのロックをすばやく解除します。</string>
<string name="reset_education_screens_text">教育的なヒントをリセットしました</string>
<string name="enable_education_screens_title">教育的なヒント</string>
<string name="keyboard_previous_database_credentials_summary">データベース認証画面で、切り替え前のキーボードへ自動的に戻します</string>
<string name="keyboard_previous_database_credentials_summary">データベース認証情報の画面で、切り替え前のキーボードへ自動的に戻します</string>
<string name="autofill_auto_search_summary">ウェブドメインまたはアプリケーション ID から検索結果を自動的に提案します</string>
<string name="keyboard_previous_fill_in_summary">自動キーアクションの実行後、切り替え前のキーボードへ自動的に戻します</string>
<string name="keyboard_previous_fill_in_title">自動キーアクション</string>
@@ -396,7 +394,7 @@
<string name="device_keyboard_setting_title">デバイス キーボードの設定</string>
<string name="settings_database_force_changing_master_key_next_time_summary">次回マスターキーの変更を必須にします1 回のみ)</string>
<string name="recycle_bin_summary">グループとエントリーを削除する前に「ごみ箱」グループに移動します</string>
<string name="keyboard_selection_entry_summary">エントリーを表示しているとき、Magikeyboard に入力フィールドを表示します</string>
<string name="keyboard_selection_entry_summary">エントリーを開いているとき、Magikeyboard に入力フィールドを表示します</string>
<string name="lock_database_show_button_title">ロックボタンを表示</string>
<string name="download_finalization">終了しています…</string>
<string name="download_initialization">初期化しています…</string>
@@ -407,7 +405,7 @@
<string name="clipboard_explanation_summary">デバイスのクリップボードを使用して、エントリーのフィールドをコピーします</string>
<string name="html_text_dev_feature_work_hard">この機能をすばやくリリースするために開発に勤しんでいます。</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_attachment">%1$s をダウンロード</string>
<string name="contribute">貢献</string>
@@ -422,7 +420,7 @@
<string name="allow_no_password_title">空のマスターキーを許可</string>
<string name="autofill_auto_search_title">自動検索</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">圧縮</string>
<string name="other">その他</string>
@@ -436,13 +434,13 @@
<string name="enable">有効にする</string>
<string name="allow_copy_password_warning">警告:クリップボードはすべてのアプリで共有されます。機密データがコピーされた場合、他のソフトウェアがデータを復元する可能性があります。</string>
<string name="allow_copy_password_summary">エントリーのパスワードと保護されたフィールドを、クリップボードにコピーすることを許可します</string>
<string name="education_donation_summary">安定性やセキュリティを向上させ、機能を追加することに協力します。</string>
<string name="education_donation_summary">安定性やセキュリティを向上させ、機能を追加することを支援します。</string>
<string name="education_donation_title">参加</string>
<string name="education_field_copy_title">フィールドをコピー</string>
<string name="education_read_only_title">データベースの書き込みを禁止</string>
<string name="education_entry_new_field_summary">追加フィールドを登録し、値を追加し、必要に応じて保護します。</string>
<string name="education_entry_new_field_title">カスタム フィールドを追加</string>
<string name="education_entry_edit_summary">カスタム フィールドを用いてエントリーを編集します。共有データは異なるエントリーのフィールド間で参照することができます。</string>
<string name="education_entry_edit_summary">カスタム フィールドを使ってエントリーを編集します。共有データは異なるエントリーのフィールド間で参照することができます。</string>
<string name="education_entry_edit_title">エントリーを編集</string>
<string name="education_biometric_title">生体認証によるロック解除</string>
<string name="education_create_database_summary">最初のパスワード管理ファイルを作成します。</string>
@@ -456,8 +454,8 @@
<string name="encryption_chacha20">ChaCha20</string>
<string name="kdf_Argon2">Argon2</string>
<string name="kdf_AES">AES</string>
<string name="list_password_generator_options_summary">パスワード ジェネレータが用いる文字種を設定します</string>
<string name="keyboard_previous_database_credentials_title">データベース認証画面</string>
<string name="list_password_generator_options_summary">パスワードの生成に使う文字種を設定します</string>
<string name="keyboard_previous_database_credentials_title">データベース認証情報の画面</string>
<string name="keyboard_change">キーボードの切り替え</string>
<string name="keyboard_keys_category">キー</string>
<string name="keyboard_theme_title">キーボードのテーマ</string>
@@ -479,7 +477,7 @@
<string name="database_data_compression_title">データ圧縮</string>
<string name="file_name">ファイル名</string>
<string name="unavailable_feature_hardware">対応するハードウェアが見つかりませんでした。</string>
<string name="unavailable_feature_version">Android のバージョン %1$s が、必要な最小バージョン %2$s を満たしていません</string>
<string name="unavailable_feature_version">デバイスは Android %1$s を実行していますが、%2$s 以降が必要です</string>
<string name="unavailable_feature_text">この機能を起動できませんでした。</string>
<string name="biometric_unlock_enable_summary">生体情報をスキャンしてデータベースを開くことができるようにします</string>
<string name="biometric_unlock_enable_title">生体認証によるロック解除</string>

View File

@@ -64,7 +64,7 @@
<string name="entry_user_name">아이디</string>
<string name="error_arc4">Arcfour 스트림 암호는 지원되지 않습니다.</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_path">경로가 확실한지 확인하십시오.</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="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_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_path">Nederīgs ceļš.</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_pass_match">പാസ്‌വേഡുകൾ പൊരുത്തപ്പെടുന്നില്ല.</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="entry_user_name">ഉപയോക്തൃനാമം</string>
<string name="entry_url">URL</string>
@@ -154,12 +154,12 @@
<string name="encryption_chacha20">ChaCha20</string>
<string name="encryption_twofish">Twofish</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="autofill_auto_search_title">സ്വയം തിരയൽ</string>
<string name="keyboard_change">കീബോർഡ് മാറ്റുക</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="max_history_items_title">പരമാവധി നമ്പർ</string>
<string name="recycle_bin_title">റീസൈക്കിൾ ബിനിൻ്റെ ഉപയോഗം</string>

View File

@@ -64,7 +64,7 @@
<string name="entry_user_name">Brukernavn</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_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_path">Ugyldig sti.</string>
<string name="error_no_name">Et navn er påkrevd.</string>
@@ -213,8 +213,6 @@
<string name="path">Sti</string>
<string name="assign_master_key">Tildel en hovednøkkel</string>
<string name="create_keepass_file">Opprett ny KeePass-fil</string>
<string name="full_file_path_enable_title">Filsti</string>
<string name="full_file_path_enable_summary">Vis hele filstien</string>
<string name="recycle_bin_title">Bruk papirkurv</string>
<string name="recycle_bin_summary">Flytt en gruppe eller oppføring til \"Papirkurv\" før sletting</string>
<string name="monospace_font_fields_enable_title">Feltskrift</string>
@@ -387,7 +385,7 @@
<string name="database_data_compression_summary">Datakomprimering reduserer databasens størrelse.</string>
<string name="compression">Komprimering</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="menu_empty_recycle_bin">Tøm papirkurven</string>
<string name="command_execution">Kjører kommandoen…</string>
@@ -401,7 +399,7 @@
<string name="download_attachment">Last ned %1$s</string>
<string name="download_progression">Underveis: %1$d%%</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="auto_focus_search_title">Hurtigsøk</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="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_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_path">Zorg ervoor dat het pad juist is.</string>
<string name="error_no_name">Voer een naam in.</string>
@@ -238,8 +238,6 @@
<string name="path">Pad</string>
<string name="assign_master_key">Hoofdsleutel toewijzen</string>
<string name="create_keepass_file">Nieuwe database aanmaken</string>
<string name="full_file_path_enable_title">Bestandspad</string>
<string name="full_file_path_enable_summary">Volledig bestandspad tonen</string>
<string name="recycle_bin_title">Prullenbak gebruiken</string>
<string name="recycle_bin_summary">Verplaatst groepen en items naar \"Prullenbak\" voordat ze worden verwijderd</string>
<string name="monospace_font_fields_enable_title">Veldlettertype</string>
@@ -425,7 +423,7 @@
<string name="database_custom_color_title">Aangepaste databasekleur</string>
<string name="compression">Compressie</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="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>
@@ -433,7 +431,7 @@
<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_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_progression">Voortgang: %1$d%%</string>
<string name="download_initialization">Initialiseren…</string>

View File

@@ -56,7 +56,7 @@
<string name="entry_user_name">Brukaramn</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_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_path">Ugyldig stig.</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="style_choose_summary">ਐਪ ਵਿੱਚ ਵਰਤਿਆ ਥੀਮ</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_progression">ਜਾਰੀ ਹੈ: %1$d%%</string>
<string name="download_initialization">…ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
@@ -22,7 +22,6 @@
<string name="clear_clipboard_notification_title">ਬੰਦ ਕਰਨ ਉੱਤੇ ਸਾਫ਼ ਕਰੋ</string>
<string name="disable">ਅਸਮਰੱਥ</string>
<string name="enable">ਸਮਰੱਥ</string>
<string name="full_file_path_enable_title">ਫ਼ਾਇਲ ਦਾ ਮਾਰਗ</string>
<string name="assign_master_key">ਮਾਸਟਰ ਕੁੰਜੀ ਦਿਓ</string>
<string name="path">ਮਾਰਗ</string>
<string name="file_name">ਫ਼ਾਇਲ ਦਾ ਨਾਂ</string>
@@ -156,7 +155,7 @@
<string name="error_invalid_OTP">ਗ਼ਲਤ OTP ਭੇਤ ਹੈ।</string>
<string name="error_invalid_path">ਪਾਥ ਦੇ ਠੀਕ ਹੋਣ ਨੂੰ ਯਕੀਨੀ ਬਣਾਓ।</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_arc4">Arcfour ਸਟਰੀਮ ਸੀਫ਼ਰ ਸਹਾਇਕ ਨਹੀਂ ਹੈ।</string>
<string name="entry_user_name">ਵਰਤੋਂਕਾਰ-ਨਾਂ</string>

View File

@@ -55,7 +55,7 @@
<string name="entry_user_name">Nazwa użytkownika</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_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_path">Upewnij się, że ścieżka jest prawidłowa.</string>
<string name="error_no_name">Wpisz nazwę.</string>
@@ -193,7 +193,7 @@
<string name="warning_empty_password">Kontynuować bez ochrony odblokowującej hasło\?</string>
<string name="warning_no_encryption_key">Kontynuować bez klucza szyfrowania\?</string>
<string name="version_label">Wersja %1$s</string>
<string name="configure_biometric">Skanowanie odcisków palców jest obsługiwane, ale nie skonfigurowane.</string>
<string name="configure_biometric">Skanowanie odcisków palców jest obsługiwane, ale nie jest skonfigurowane.</string>
<string name="encrypted_value_stored">Zapisano zaszyfrowane hasło</string>
<string name="sort_groups_before">Grupy poprzednie</string>
<string name="open_biometric_prompt_unlock_database">Otwórz żądanie biometryczne, aby odblokować bazę danych</string>
@@ -216,7 +216,7 @@
<string name="list_password_generator_options_summary">Ustaw dozwolone znaki generatora haseł</string>
<string name="clipboard">Schowek</string>
<string name="clipboard_notifications_title">Powiadomienia ze schowka</string>
<string name="clipboard_notifications_summary">Włącz powiadomienia schowka, aby skopiować pola podczas wyświetlania wpisu</string>
<string name="clipboard_notifications_summary">Pokaż powiadomienia schowka, aby skopiować pola podczas przeglądania wpisu</string>
<string name="clipboard_warning">Jeśli automatyczne usuwanie schowka nie powiedzie się, ręcznie usuń jego historię.</string>
<string name="lock">Blokada</string>
<string name="lock_database_screen_off_title">Blokada ekranu</string>
@@ -228,14 +228,12 @@
<string name="biometric_delete_all_key_summary">Usuń wszystkie klucze szyfrowania związane z rozpoznawaniem linii papilarnych</string>
<string name="biometric_delete_all_key_warning">Czy usunąć wszystkie klucze szyfrowania związane z rozpoznawaniem biometrycznym\?</string>
<string name="unavailable_feature_text">Nie można uruchomić tej funkcji.</string>
<string name="unavailable_feature_version">Twoja wersja Androida %1$s nie spełnia wymaganej minimalnej wersji %2$s.</string>
<string name="unavailable_feature_version">Urządzenie pracuje na systemie Android %1$s, ale wymaga wersji %2$s lub nowszej.</string>
<string name="unavailable_feature_hardware">Nie można znaleźć odpowiedniego sprzętu.</string>
<string name="file_name">Nazwa pliku</string>
<string name="path">Ścieżka</string>
<string name="assign_master_key">Przypisz klucz główny</string>
<string name="create_keepass_file">Utwórz nową bazę danych</string>
<string name="full_file_path_enable_title">Ścieżka dostępu do plików</string>
<string name="full_file_path_enable_summary">Wyświetl pełną ścieżkę do pliku</string>
<string name="recycle_bin_title">Wykorzystaj kosz</string>
<string name="recycle_bin_summary">Przenosi grupy i wpisy do grupy \"Kosz\" przed usunięciem</string>
<string name="monospace_font_fields_enable_title">Pole czcionka</string>
@@ -253,7 +251,7 @@
<string name="magic_keyboard_title">Magikeyboard</string>
<string name="magic_keyboard_explanation_summary">Aktywuj niestandardową klawiaturę wypełniającą hasła i wszystkie pola tożsamości</string>
<string name="allow_no_password_title">Zezwalaj na brak klucza głównego</string>
<string name="allow_no_password_summary">Włącz przycisk \"Otwórz\", jeśli nie wybrano uwierzytelni</string>
<string name="allow_no_password_summary">Umożliwia naciśnięcie przycisku \"Otwórz\", jeśli nie wybrano żadnych poświadcz</string>
<string name="enable_read_only_title">Ochrona przed zapisem</string>
<string name="enable_read_only_summary">Domyślnie otwarte bazy danych są tylko do odczytu</string>
<string name="enable_education_screens_title">Wskazówki edukacyjne</string>
@@ -271,7 +269,7 @@
\nGrupy (~ foldery) organizują wpisy w bazie danych.</string>
<string name="education_search_title">Przeszukuj wpisy</string>
<string name="education_search_summary">Wprowadź tytuł, nazwę użytkownika lub zawartość innych pól, aby odzyskać swoje hasła.</string>
<string name="education_biometric_title">Odblokuj bazę danych za pomocą odcisku palca</string>
<string name="education_biometric_title">Biometryczne odblokowanie bazy danych</string>
<string name="education_biometric_summary">Połącz swoje hasło z zeskanowanym odciskiem palca, aby szybko odblokować bazę danych.</string>
<string name="education_entry_edit_title">Edytuj wpis</string>
<string name="education_entry_edit_summary">Edytuj swój wpis za pomocą pól niestandardowych. Dane puli mogą być przywoływane między różnymi polami wprowadzania.</string>
@@ -405,7 +403,7 @@
<string name="database_custom_color_title">Niestandardowy kolor bazy danych</string>
<string name="compression">Kompresja</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="error_invalid_OTP">Nieprawidłowy klucz tajny OTP.</string>
<string name="error_disallow_no_credentials">Należy ustawić co najmniej jedno poświadczenie.</string>
@@ -442,17 +440,17 @@
<string name="download_initialization">Inicjowanie…</string>
<string name="download_progression">W trakcie realizacji: %1$d%%</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_summary">Wygasłe wpisy są ukryte</string>
<string name="hide_expired_entries_summary">Wygasłe wpisy nie są wyświetlane</string>
<string name="contact">Kontakt</string>
<string name="html_about_contribution">Aby &lt;strong&gt;zachować naszą wolność&lt;/strong&gt;, &lt;strong&gt;sprawdzać błędy&lt;/strong&gt;, &lt;strong&gt;dodać funkcje&lt;/strong&gt; i &lt;strong&gt;by być zawsze aktywnym&lt;/strong&gt;, liczymy na twój &lt;strong&gt;wkład&lt;/strong&gt;.</string>
<string name="auto_focus_search_title">Szybkie wyszukiwanie</string>
<string name="auto_focus_search_summary">Wyszukiwanie po otwarciu bazy danych</string>
<string name="remember_database_locations_title">Zapisz lokalizację baz danych</string>
<string name="remember_database_locations_summary">Zapamiętaj lokalizację baz danych</string>
<string name="remember_keyfile_locations_title">Zapisz lokalizację plików kluczy</string>
<string name="remember_keyfile_locations_summary">Zapamiętaj lokalizację plików kluczy baz danych</string>
<string name="remember_database_locations_title">Zapamiętaj lokalizacje baz danych</string>
<string name="remember_database_locations_summary">Śledzi, gdzie przechowywane są bazy danych</string>
<string name="remember_keyfile_locations_title">Zapamiętaj lokalizacje plików kluczy</string>
<string name="remember_keyfile_locations_summary">Śledzi, gdzie przechowywane są pliki z kluczami</string>
<string name="show_recent_files_title">Pokaż najnowsze pliki</string>
<string name="show_recent_files_summary">Pokaż lokalizacje najnowszych baz danych</string>
<string name="hide_broken_locations_title">Ukryj uszkodzone łącza do bazy danych</string>

View File

@@ -56,7 +56,7 @@
<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_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_path">Certifique-se de que o caminho está correto.</string>
<string name="error_no_name">Digite um nome.</string>
@@ -229,8 +229,6 @@
<string name="path">Caminho</string>
<string name="assign_master_key">Defina uma chave mestre</string>
<string name="create_keepass_file">Criar novo banco</string>
<string name="full_file_path_enable_title">Caminho do arquivo</string>
<string name="full_file_path_enable_summary">Veja o caminho inteiro do arquivo</string>
<string name="recycle_bin_title">Usar lixeira</string>
<string name="recycle_bin_summary">Mover grupos e entradas para o grupo \"Lixeira\" antes de apagar</string>
<string name="monospace_font_fields_enable_title">Fonte do Campo</string>
@@ -423,7 +421,7 @@
<string name="database_custom_color_title">Cor personalizada do banco de dados</string>
<string name="compression">Compressão</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="error_save_database">Não foi possível salvar no banco de dados.</string>
<string name="menu_save_database">Salvar banco de dados</string>
@@ -443,7 +441,7 @@
<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_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_progression">Em progresso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string>

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