mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Merge branch 'feature/File_Attachment' into develop #189
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
KeePassDX(2.8.3)
|
||||
* Add attachments
|
||||
|
||||
KeePassDX(2.8.2)
|
||||
* Fix themes / new UI
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,17 +333,12 @@ class EntryActivity : LockingActivity() {
|
||||
entryContentsView?.setHiddenProtectedValue(!mShowPassword)
|
||||
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entry.getAttachments()) { attachmentItem ->
|
||||
when (attachmentItem.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.ERROR, AttachmentState.COMPLETE -> {
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
entryContentsView?.assignAttachments(entry.getAttachments(binaryPool), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
createDocument(this, attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO Stop download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
|
||||
@@ -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,18 @@ 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.database.CompressionAlgorithm
|
||||
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 +57,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 +73,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 +96,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
|
||||
|
||||
@@ -221,10 +232,16 @@ class EntryEditActivity : LockingActivity(),
|
||||
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 ->
|
||||
@@ -233,6 +250,10 @@ class EntryEditActivity : LockingActivity(),
|
||||
addNewCustomField()
|
||||
true
|
||||
}
|
||||
R.id.menu_add_attachment -> {
|
||||
addNewAttachment(item)
|
||||
true
|
||||
}
|
||||
R.id.menu_add_otp -> {
|
||||
setupOTP()
|
||||
true
|
||||
@@ -242,6 +263,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() }
|
||||
@@ -273,6 +298,41 @@ 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 -> {
|
||||
// When only one attachment is allowed
|
||||
if (!mAllowMultipleAttachments) {
|
||||
entryEditContentsView?.clearAttachments()
|
||||
}
|
||||
entryEditContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.IN_PROGRESS -> {
|
||||
entryEditContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.COMPLETE -> {
|
||||
entryEditContentsView?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
AttachmentState.ERROR -> {
|
||||
mDatabase?.removeAttachmentIfNotUsed(entryAttachmentState.attachment)
|
||||
entryEditContentsView?.removeAttachment(entryAttachmentState)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mAttachmentFileBinderManager?.unregisterProgressTask()
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun populateViewsWithEntry(newEntry: Entry) {
|
||||
@@ -298,11 +358,14 @@ class EntryEditActivity : LockingActivity(),
|
||||
assignExtraFields(newEntry.customFields.mapTo(ArrayList()) {
|
||||
Field(it.key, it.value)
|
||||
}, mFocusedEditExtraField)
|
||||
assignAttachments(newEntry.getAttachments()) { attachment ->
|
||||
|
||||
mDatabase?.binaryPool?.let { binaryPool ->
|
||||
assignAttachments(newEntry.getAttachments(binaryPool), StreamDirection.UPLOAD) { attachment ->
|
||||
newEntry.removeAttachment(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateEntryWithViews(newEntry: Entry) {
|
||||
|
||||
@@ -321,9 +384,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,6 +426,63 @@ class EntryEditActivity : LockingActivity(),
|
||||
|
||||
override fun onNewCustomFieldCanceled(label: String, protection: Boolean) {}
|
||||
|
||||
/**
|
||||
* Add a new attachment
|
||||
*/
|
||||
private fun addNewAttachment(item: MenuItem) {
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onMenuItemClick(item)
|
||||
}
|
||||
|
||||
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
||||
if (attachmentToUploadUri != null && fileName != null) {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
||||
if (attachmentToUploadUri != null && attachment != null) {
|
||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
||||
val compression = mDatabase?.compressionForNewEntry() ?: false
|
||||
mDatabase?.buildNewBinary(applicationContext.filesDir, false, compression)?.let { binaryAttachment ->
|
||||
val entryAttachment = Attachment(fileName, binaryAttachment)
|
||||
// Ask to replace the current attachment
|
||||
if ((mDatabase?.allowMultipleAttachments != true && entryEditContentsView?.containsAttachment() == true) ||
|
||||
entryEditContentsView?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
||||
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
||||
.show(supportFragmentManager, "replacementFileFragment")
|
||||
} else {
|
||||
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
mSelectFileHelper?.onActivityResultCallback(requestCode, resultCode, data) { uri ->
|
||||
uri?.let { attachmentToUploadUri ->
|
||||
// TODO Async to get the name
|
||||
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
||||
documentFile.name?.let { fileName ->
|
||||
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
||||
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
||||
.show(supportFragmentManager, "fileTooBigFragment")
|
||||
} else {
|
||||
buildNewAttachment(attachmentToUploadUri, fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up OTP (HOTP or TOTP) and add it as extra field
|
||||
*/
|
||||
private fun setupOTP() {
|
||||
// Retrieve the current otpElement if exists
|
||||
// and open the dialog to set up the OTP
|
||||
|
||||
@@ -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
|
||||
@@ -82,7 +82,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
|
||||
private var mDatabaseFileUri: Uri? = null
|
||||
|
||||
private var mOpenFileHelper: OpenFileHelper? = null
|
||||
private var mSelectFileHelper: SelectFileHelper? = null
|
||||
|
||||
private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null
|
||||
|
||||
@@ -108,10 +108,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
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 +389,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 +445,7 @@ class FileDatabaseSelectActivity : SpecialModeActivity(),
|
||||
openDatabaseButtonView!!,
|
||||
{tapTargetView ->
|
||||
tapTargetView?.let {
|
||||
mOpenFileHelper?.openFileOnClickViewListener?.onClick(it)
|
||||
mSelectFileHelper?.selectFileOnClickViewListener?.onClick(it)
|
||||
}
|
||||
},
|
||||
{}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -32,6 +32,14 @@ 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 putItem(item: Item) {
|
||||
val previousSize = itemsList.size
|
||||
if (itemsList.contains(item)) {
|
||||
@@ -46,6 +54,13 @@ abstract class AnimatedItemsAdapter<Item, T: RecyclerView.ViewHolder>(val contex
|
||||
onListSizeChangedListener?.invoke(previousSize, itemsList.size)
|
||||
}
|
||||
|
||||
open fun removeItem(item: Item) {
|
||||
if (itemsList.contains(item)) {
|
||||
mItemToRemove = item
|
||||
notifyItemChanged(itemsList.indexOf(item))
|
||||
}
|
||||
}
|
||||
|
||||
fun onBindDeleteButton(holder: T, deleteButton: View, item: Item, position: Int) {
|
||||
deleteButton.apply {
|
||||
visibility = View.VISIBLE
|
||||
|
||||
@@ -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) {
|
||||
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, entryAttachment, position)
|
||||
onBindDeleteButton(holder, this, entryAttachmentState, position)
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnClickListener(null)
|
||||
}
|
||||
StreamDirection.DOWNLOAD -> {
|
||||
holder.binaryFileProgressIcon.isActivated = false
|
||||
holder.binaryFileProgressContainer.visibility = View.VISIBLE
|
||||
holder.binaryFileDeleteButton.visibility = View.GONE
|
||||
holder.binaryFileProgress.apply {
|
||||
visibility = when (entryAttachment.downloadState) {
|
||||
visibility = when (entryAttachmentState.downloadState) {
|
||||
AttachmentState.NULL, AttachmentState.COMPLETE, AttachmentState.ERROR -> View.GONE
|
||||
AttachmentState.START, AttachmentState.IN_PROGRESS -> View.VISIBLE
|
||||
}
|
||||
progress = entryAttachment.downloadProgression
|
||||
progress = entryAttachmentState.downloadProgression
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
onItemClickListener?.invoke(entryAttachment)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -64,6 +64,10 @@ class DeleteNodesRunnable(context: Context,
|
||||
} else {
|
||||
database.deleteEntry(currentNode)
|
||||
}
|
||||
// Remove the oldest attachments
|
||||
currentNode.getAttachments(database.binaryPool).forEach {
|
||||
database.removeAttachmentIfNotUsed(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
val newEntryAttachments = mNewEntry.getAttachments(database.binaryPool)
|
||||
val attachmentsToRemove = ArrayList<Attachment>(oldEntryAttachments)
|
||||
// Not use equals because only check name
|
||||
newEntryAttachments.forEach { newAttachment ->
|
||||
oldEntryAttachments.forEach { oldAttachment ->
|
||||
if (oldAttachment.name == newAttachment.name
|
||||
&& oldAttachment.binaryAttachment == newAttachment.binaryAttachment)
|
||||
attachmentsToRemove.remove(oldAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
// Update entry with new values
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,12 @@ 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
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||
@@ -52,6 +51,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 +157,17 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
fun compressionForNewEntry(): Boolean {
|
||||
if (mDatabaseKDB != null)
|
||||
return false
|
||||
// Default compression not necessary if stored in header
|
||||
mDatabaseKDBX?.let {
|
||||
return it.compressionAlgorithm == CompressionAlgorithm.GZip
|
||||
&& it.kdbxVersion.toKotlinLong() < DatabaseHeaderKDBX.FILE_VERSION_32_4.toKotlinLong()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun updateDataBinaryCompression(oldCompression: CompressionAlgorithm,
|
||||
newCompression: CompressionAlgorithm) {
|
||||
mDatabaseKDBX?.changeBinaryCompression(oldCompression, newCompression)
|
||||
@@ -428,6 +439,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 {
|
||||
@@ -800,7 +842,7 @@ class Database {
|
||||
rootGroup?.doForEachChildAndForIt(
|
||||
object : NodeHandler<Entry>() {
|
||||
override fun operate(node: Entry): Boolean {
|
||||
removeOldestEntryHistory(node)
|
||||
removeOldestEntryHistory(node, binaryPool)
|
||||
return true
|
||||
}
|
||||
},
|
||||
@@ -808,7 +850,8 @@ class Database {
|
||||
override fun operate(node: Group): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun removeEachEntryHistory() {
|
||||
@@ -829,9 +872,8 @@ class Database {
|
||||
/**
|
||||
* 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) {
|
||||
@@ -844,7 +886,7 @@ class Database {
|
||||
while (true) {
|
||||
var historySize: Long = 0
|
||||
for (entryHistory in entry.getHistory()) {
|
||||
historySize += entryHistory.getSize()
|
||||
historySize += entryHistory.getSize(binaryPool)
|
||||
}
|
||||
|
||||
if (historySize > maxSize) {
|
||||
|
||||
@@ -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): ArrayList<Attachment> {
|
||||
val attachments = ArrayList<Attachment>()
|
||||
entryKDB?.getAttachments()?.let {
|
||||
attachments.addAll(it)
|
||||
}
|
||||
|
||||
entryKDBX?.binaries?.let { binariesKDBX ->
|
||||
for ((key, value) in binariesKDBX) {
|
||||
attachments.add(EntryAttachment(key, value))
|
||||
entryKDBX?.getAttachments(binaryPool)?.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> {
|
||||
@@ -380,8 +378,8 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
||||
entryKDBX?.removeOldestEntryFromHistory()
|
||||
}
|
||||
|
||||
fun getSize(): Long {
|
||||
return entryKDBX?.size ?: 0L
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
return entryKDBX?.getSize(binaryPool) ?: 0L
|
||||
}
|
||||
|
||||
fun containsCustomData(): Boolean {
|
||||
|
||||
@@ -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,24 +72,44 @@ 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()
|
||||
GZIPOutputStream(FileOutputStream(fileBinaryCompress)).use { outputStream ->
|
||||
getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
}
|
||||
}
|
||||
// Remove unGzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryCompress.renameTo(concreteDataFile)) {
|
||||
@@ -102,25 +120,19 @@ 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())
|
||||
FileOutputStream(fileBinaryDecompress).use { outputStream ->
|
||||
getUnGzipInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(bufferSize) { buffer ->
|
||||
outputStream.write(buffer)
|
||||
}
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
|
||||
}
|
||||
}
|
||||
// Remove gzip file
|
||||
if (concreteDataFile.delete()) {
|
||||
if (fileBinaryDecompress.renameTo(concreteDataFile)) {
|
||||
@@ -131,33 +143,6 @@ class BinaryAttachment : Parcelable {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun clear() {
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdInt
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.stream.NullOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.security.DigestOutputStream
|
||||
@@ -249,6 +250,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
|
||||
|
||||
|
||||
@@ -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.None -> {}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
// To compress, create a new binary with file
|
||||
binary.compress(BUFFER_SIZE_BYTES)
|
||||
// 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 -> {
|
||||
// To decompress, create a new binary with file
|
||||
binary.decompress(BUFFER_SIZE_BYTES)
|
||||
decompressAllBinaries()
|
||||
}
|
||||
CompressionAlgorithm.GZip -> {
|
||||
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).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
|
||||
|
||||
@@ -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,30 @@ class EntryKDB : EntryVersioned<Int, UUID, GroupKDB, EntryKDB>, NodeKDBInterface
|
||||
override val type: Type
|
||||
get() = Type.ENTRY
|
||||
|
||||
fun getAttachments(): ArrayList<Attachment> {
|
||||
return ArrayList<Attachment>().apply {
|
||||
val binary = binaryData
|
||||
if (binary != null)
|
||||
add(Attachment(binaryDescription, binary))
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return binaryData != null
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: Attachment) {
|
||||
this.binaryDescription = attachment.name
|
||||
this.binaryData = attachment.binaryAttachment
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: Attachment) {
|
||||
if (this.binaryDescription == attachment.name) {
|
||||
this.binaryDescription = ""
|
||||
this.binaryData = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Size of byte buffer needed to hold this struct. */
|
||||
|
||||
@@ -31,11 +31,13 @@ import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||
import com.kunzisoft.keepass.database.element.node.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.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.database.BinaryPool
|
||||
import com.kunzisoft.keepass.utils.ParcelableUtil
|
||||
import com.kunzisoft.keepass.utils.UnsignedLong
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.LinkedHashMap
|
||||
|
||||
class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInterface {
|
||||
@@ -60,8 +62,9 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
var iconCustom = IconImageCustom.UNKNOWN_ICON
|
||||
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,8 +73,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
var additional = ""
|
||||
var tags = ""
|
||||
|
||||
val size: Long
|
||||
get() {
|
||||
fun getSize(binaryPool: BinaryPool): Long {
|
||||
var size = FIXED_LENGTH_SIZE
|
||||
|
||||
for (entry in fields.entries) {
|
||||
@@ -79,10 +81,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
size += entry.value.length().toLong()
|
||||
}
|
||||
|
||||
for ((key, value) in binaries) {
|
||||
size += key.length.toLong()
|
||||
size += value.length()
|
||||
}
|
||||
size += getAttachmentsSize(binaryPool)
|
||||
|
||||
size += autoType.defaultSequence.length.toLong()
|
||||
for ((key, value) in autoType.entrySet()) {
|
||||
@@ -91,7 +90,7 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
}
|
||||
|
||||
for (entry in history) {
|
||||
size += entry.size
|
||||
size += entry.getSize(binaryPool)
|
||||
}
|
||||
|
||||
size += overrideURL.length.toLong()
|
||||
@@ -110,7 +109,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 +127,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 +166,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 +283,38 @@ class EntryKDBX : EntryVersioned<UUID, UUID, GroupKDBX, EntryKDBX>, NodeKDBXInte
|
||||
fields[label] = value
|
||||
}
|
||||
|
||||
fun putProtectedBinary(key: String, value: BinaryAttachment) {
|
||||
binaries[key] = value
|
||||
fun getAttachments(binaryPool: BinaryPool): ArrayList<Attachment> {
|
||||
val entryAttachmentList = ArrayList<Attachment>()
|
||||
for ((label, poolId) in binaries) {
|
||||
binaryPool[poolId]?.let { binary ->
|
||||
entryAttachmentList.add(Attachment(label, binary))
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,9 +233,11 @@ 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
|
||||
when (fieldId) {
|
||||
@@ -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,15 +941,28 @@ 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)
|
||||
|
||||
return when {
|
||||
ref != null -> {
|
||||
xpp.next() // Consume end tag
|
||||
val id = Integer.parseInt(ref)
|
||||
return mDatabase.binaryPool[id]
|
||||
// A ref is not necessarily an index in Database V3.1
|
||||
mDatabase.binaryPool[id]
|
||||
}
|
||||
key != null -> {
|
||||
createBinary(key.toIntOrNull(), xpp)
|
||||
}
|
||||
else -> {
|
||||
// New binary to retrieve
|
||||
createBinary(null, xpp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New binary to retrieve
|
||||
else {
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
private fun createBinary(binaryId: Int?, xpp: XmlPullParser): BinaryAttachment? {
|
||||
var compressed = false
|
||||
var protected = false
|
||||
|
||||
@@ -973,22 +980,15 @@ class DatabaseInputKDBX(cacheDirectory: File,
|
||||
|
||||
val base64 = readString(xpp)
|
||||
if (base64.isEmpty())
|
||||
return BinaryAttachment()
|
||||
return null
|
||||
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 {
|
||||
// Build the new binary and compress
|
||||
val binaryAttachment = mDatabase.buildNewBinary(cacheDirectory, protected, compressed, binaryId)
|
||||
binaryAttachment.getOutputDataStream().use { outputStream ->
|
||||
outputStream.write(data)
|
||||
BinaryAttachment(file, protected, compressed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return binaryAttachment
|
||||
}
|
||||
|
||||
@Throws(IOException::class, XmlPullParserException::class)
|
||||
|
||||
@@ -47,20 +47,27 @@ 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 ->
|
||||
// if was compressed in cache, uncompress it
|
||||
protectedBinary.getInputDataStream().use { inputStream ->
|
||||
inputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
|
||||
dataOutputStream.write(buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataOutputStream.write(DatabaseHeaderKDBX.PwDbInnerHeaderV4Fields.EndOfHeader.toInt())
|
||||
dataOutputStream.writeInt(0)
|
||||
|
||||
@@ -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,25 +548,24 @@ 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) {
|
||||
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(key))
|
||||
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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
private fun writeDeletedObjects(value: List<DeletedObject>) {
|
||||
|
||||
@@ -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,
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
enum class StreamDirection {
|
||||
UPLOAD, DOWNLOAD
|
||||
}
|
||||
@@ -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>) {
|
||||
attachmentNotificationList.forEach { attachmentNotification ->
|
||||
mActionTaskListeners.forEach { actionListener ->
|
||||
actionListener.onAttachmentProgress(entry.key, entry.value.entryAttachment)
|
||||
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 {
|
||||
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))
|
||||
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>()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun getUriInputStream(contentResolver: ContentResolver, fileUri: Uri?): InputStream? {
|
||||
if (fileUri == null)
|
||||
|
||||
@@ -35,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.*
|
||||
@@ -69,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
|
||||
@@ -105,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
|
||||
}
|
||||
@@ -315,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: ArrayList<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)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
|
||||
@@ -38,9 +38,11 @@ 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.database.element.Attachment
|
||||
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
|
||||
}
|
||||
|
||||
@@ -278,13 +280,39 @@ class EntryEditContentsView @JvmOverloads constructor(context: Context,
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun assignAttachments(attachments: ArrayList<EntryAttachment>,
|
||||
onDeleteItem: (attachment: EntryAttachment)->Unit) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments)
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item)
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: ArrayList<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onDeleteItem: (attachment: Attachment)->Unit) {
|
||||
attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onDeleteButtonClickListener = { 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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
app/src/main/res/drawable/ic_file_stream_white_24dp.xml
Normal file
7
app/src/main/res/drawable/ic_file_stream_white_24dp.xml
Normal 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>
|
||||
5
app/src/main/res/drawable/ic_file_upload_white_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_file_upload_white_24dp.xml
Normal 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" />
|
||||
@@ -31,7 +31,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"
|
||||
@@ -95,7 +94,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_file_download_white_24dp"
|
||||
android:src="@drawable/ic_file_stream_white_24dp"
|
||||
android:contentDescription="@string/download"
|
||||
style="@style/KeepassDXStyle.ImageButton.Simple" />
|
||||
<ProgressBar
|
||||
|
||||
@@ -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,18 @@
|
||||
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"
|
||||
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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<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_file_not_create">Soubor se nedaří vytvořit</string>
|
||||
<string name="error_invalid_db">Nelze přečíst databázi.</string>
|
||||
<string name="error_invalid_path">Neplatná cesta.</string>
|
||||
<string name="error_no_name">Vložte jméno.</string>
|
||||
@@ -419,7 +419,7 @@
|
||||
<string name="database_custom_color_title">Vlastní barva databáze</string>
|
||||
<string name="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,7 +440,7 @@
|
||||
<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">Klepnout pro otevření souboru</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="contact">Kontakt</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -419,7 +419,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,7 +440,7 @@
|
||||
<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="contact">Kontakt</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -435,7 +435,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,7 +456,7 @@
|
||||
<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="style_choose_title">App-Design</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -418,7 +418,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 +442,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -397,7 +397,7 @@
|
||||
<string name="error_create_database">No fue posible crear el archivo de base de datos.</string>
|
||||
<string name="html_about_contribution">Parar lograr <strong>mantener nuestra libertad</strong>, <strong>corregir errores</strong>, <strong>agregar características</strong> y <strong>siempre estar activos</strong>, contamos con tu <strong>contribución</strong>.</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 +407,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<string name="entry_user_name">Nom d’utilisateur</string>
|
||||
<string name="error_arc4">Le chiffrement de flux Arcfour n’est 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 d’accès.</string>
|
||||
<string name="error_no_name">Saisir un nom.</string>
|
||||
@@ -435,7 +435,7 @@
|
||||
<string name="database_custom_color_title">Couleur de la base de données</string>
|
||||
<string name="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 l’appareil</string>
|
||||
<string name="error_save_database">Impossible d’enregistrer la base de données.</string>
|
||||
<string name="menu_save_database">Enregistrer la base de données</string>
|
||||
@@ -456,7 +456,7 @@
|
||||
<string name="download_initialization">Initialisation…</string>
|
||||
<string name="download_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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -233,12 +233,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>
|
||||
@@ -469,7 +469,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -384,7 +384,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 +405,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -448,7 +448,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>
|
||||
@@ -461,7 +461,7 @@
|
||||
<string name="keyboard_auto_go_action_summary">Dopo la pressione del tasto \"Campo\" invia il tasto \"Vai\"</string>
|
||||
<string name="keyboard_auto_go_action_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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -407,7 +407,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 +422,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -387,7 +387,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 +401,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -425,7 +425,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 +433,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -156,7 +156,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -405,7 +405,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,7 +442,7 @@
|
||||
<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="contact">Kontakt</string>
|
||||
|
||||
@@ -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>
|
||||
@@ -423,7 +423,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 +443,7 @@
|
||||
<string name="autofill_auto_search_summary">Sugerir resultados de pesquisa de domínios da internet ou de aplicações automaticamente</string>
|
||||
<string name="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>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<string name="entry_user_name">Nome de utilizador</string>
|
||||
<string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string>
|
||||
<string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string>
|
||||
<string name="error_file_not_create">Não foi possível criar o ficheiro:</string>
|
||||
<string name="error_file_not_create">Não foi possível criar o ficheiro</string>
|
||||
<string name="error_invalid_db">Não foi possível ler a base de dados.</string>
|
||||
<string name="error_invalid_path">Certifique-se que o caminho é válido.</string>
|
||||
<string name="error_no_name">Introduza um nome.</string>
|
||||
@@ -420,7 +420,7 @@
|
||||
<string name="html_about_contribution">Para <strong>manter a liberdade</strong>, <strong>solucionar bugs</strong>, <strong>adicionar funções</strong> e <strong>para sermos sempre ativoa</strong>, contamos com sua <strong>contribuição</strong>.</string>
|
||||
<string name="biometric_unlock_enable_title">Desbloqueio por biométrico</string>
|
||||
<string name="entry_attachments">Anexos</string>
|
||||
<string name="compression_gzip">gzip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="discard">Descartar</string>
|
||||
<string name="remember_database_locations_summary">Lembrar o local das bases de dados</string>
|
||||
<string name="auto_focus_search_summary">Solicitar uma pesquisa quando abrir a base de dados</string>
|
||||
@@ -428,7 +428,7 @@
|
||||
<string name="download_finalization">A finalizar…</string>
|
||||
<string name="autofill_preference_title">Configurações de preenchimento automático</string>
|
||||
<string name="max_history_items_summary">Limitar a quantidade de itens do histórico por entrada</string>
|
||||
<string name="download_complete">Completo! Toque para abrir o ficheiro.</string>
|
||||
<string name="download_complete">Completo!</string>
|
||||
<string name="content_description_update_from_list">Atualizar</string>
|
||||
<string name="contribution">Contribuição</string>
|
||||
<string name="hide_broken_locations_summary">Esconder ligações quebradas na lista de bases de dados recentes</string>
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<string name="entry_user_name">Nume utilizator</string>
|
||||
<string name="error_arc4">Cifrarea fluxului Arcfour nu este acceptată.</string>
|
||||
<string name="error_can_not_handle_uri">Nu s-a putut gestiona acest URI în KeePassDX.</string>
|
||||
<string name="error_file_not_create">Nu s-a putut creea fisierul:</string>
|
||||
<string name="error_file_not_create">Nu s-a putut creea fisierul</string>
|
||||
<string name="error_invalid_db">Nu s-a putut citi baza de date.</string>
|
||||
<string name="error_invalid_path">Asigurați-vă că calea este corectă.</string>
|
||||
<string name="error_invalid_OTP">Secret OTP nevalid.</string>
|
||||
@@ -323,7 +323,7 @@
|
||||
<string name="other">Alta</string>
|
||||
<string name="compression">Compresie</string>
|
||||
<string name="compression_none">Nimic</string>
|
||||
<string name="compression_gzip">gzip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="recycle_bin">Cos de reciclare</string>
|
||||
<string name="keyboard">Tastatura</string>
|
||||
<string name="magic_keyboard_title">TastaturaMagica</string>
|
||||
@@ -418,7 +418,7 @@
|
||||
<string name="download_initialization">Inițializare …</string>
|
||||
<string name="download_progression">In progress: %1$d%%</string>
|
||||
<string name="download_finalization">Finalizare …</string>
|
||||
<string name="download_complete">Complet! Atingeți pentru a deschide fișierul.</string>
|
||||
<string name="download_complete">Complet!</string>
|
||||
<string name="encryption_rijndael">Rijndael (AES)</string>
|
||||
<string name="encryption_twofish">Twofish</string>
|
||||
<string name="encryption_chacha20">ChaCha20</string>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<string name="entry_user_name">Имя пользователя</string>
|
||||
<string name="error_arc4">Потоковый шифр Arcfour не поддерживается.</string>
|
||||
<string name="error_can_not_handle_uri">Невозможно обработать указанный URI в KeePassDX.</string>
|
||||
<string name="error_file_not_create">Невозможно создать файл:</string>
|
||||
<string name="error_file_not_create">Невозможно создать файл</string>
|
||||
<string name="error_invalid_db">Невозможно прочитать базу.</string>
|
||||
<string name="error_invalid_path">Убедитесь, что путь указан правильно.</string>
|
||||
<string name="error_no_name">Введите название.</string>
|
||||
@@ -421,7 +421,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="device_keyboard_setting_title">Настройки клавиатур устройства</string>
|
||||
<string name="error_save_database">Невозможно сохранить базу.</string>
|
||||
<string name="menu_save_database">Сохранить базу</string>
|
||||
@@ -442,7 +442,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="contact">Контактная информация</string>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<string name="entry_user_name">Meno používateľa</string>
|
||||
<string name="error_arc4">Arcfour stream šifra nieje podporovaná.</string>
|
||||
<string name="error_can_not_handle_uri">KeePassDX nevie použiť túto uri.</string>
|
||||
<string name="error_file_not_create">Neviem vytvoriť súbor:</string>
|
||||
<string name="error_file_not_create">Neviem vytvoriť súbor</string>
|
||||
<string name="error_invalid_db">Chybná databáza.</string>
|
||||
<string name="error_invalid_path">Chybná cesta.</string>
|
||||
<string name="error_no_name">Vyžaduje sa meno.</string>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<string name="entry_user_name">Användarnamn</string>
|
||||
<string name="error_arc4">Strömchiffret Arcfour stöds inte.</string>
|
||||
<string name="error_can_not_handle_uri">KeePassDX kunde inte hantera denna URI.</string>
|
||||
<string name="error_file_not_create">Kunde inte skapa filen:</string>
|
||||
<string name="error_file_not_create">Kunde inte skapa filen</string>
|
||||
<string name="error_invalid_db">Kunde inte läsa databas.</string>
|
||||
<string name="error_invalid_path">Se till att sökvägen är korrekt.</string>
|
||||
<string name="error_no_name">Ange ett namn.</string>
|
||||
@@ -418,7 +418,7 @@
|
||||
<string name="database_custom_color_title">Anpassad databasfärg</string>
|
||||
<string name="compression">Komprimering</string>
|
||||
<string name="compression_none">Ingen</string>
|
||||
<string name="compression_gzip">GZip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="magic_keyboard_explanation_summary">Aktivera ett anpassat tangentbord som innehåller dina lösenord och alla identitetsfält</string>
|
||||
<string name="device_keyboard_setting_title">Enhetens tangentbordsinställningar</string>
|
||||
<string name="education_biometric_title">Lås upp databasen med biometrik</string>
|
||||
@@ -446,7 +446,7 @@
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="hide_expired_entries_summary">Utgångna poster är dolda</string>
|
||||
<string name="hide_expired_entries_title">Dölj utgångna poster</string>
|
||||
<string name="download_complete">Färdigt! Klicka för att öppna filen.</string>
|
||||
<string name="download_complete">Färdigt!</string>
|
||||
<string name="download_finalization">Färdigställande…</string>
|
||||
<string name="download_progression">Händelse %1$d%%</string>
|
||||
<string name="download_initialization">Initiering…</string>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<string name="entry_title">Başlık</string>
|
||||
<string name="entry_url">URL</string>
|
||||
<string name="entry_user_name">Kullanıcı adı</string>
|
||||
<string name="error_file_not_create">Dosya oluşturulamadı:</string>
|
||||
<string name="error_file_not_create">Dosya oluşturulamadı</string>
|
||||
<string name="error_invalid_db">Veritabanı okunamadı.</string>
|
||||
<string name="error_invalid_path">Yolun doğru olduğundan emin olun.</string>
|
||||
<string name="error_no_name">Bir isim girin.</string>
|
||||
@@ -405,7 +405,7 @@
|
||||
<string name="database_custom_color_title">Özel veritabanı rengi</string>
|
||||
<string name="compression">Sıkıştırma</string>
|
||||
<string name="compression_none">Yok</string>
|
||||
<string name="compression_gzip">gzip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="device_keyboard_setting_title">Cihaz klavye ayarları</string>
|
||||
<string name="error_save_database">Veritabanı kaydedilemedi.</string>
|
||||
<string name="menu_save_database">Veritabanını kaydet</string>
|
||||
@@ -426,7 +426,7 @@
|
||||
<string name="download_initialization">Başlatılıyor…</string>
|
||||
<string name="download_progression">Devam ediyor: %1$d%%</string>
|
||||
<string name="download_finalization">Sonlandırılıyor…</string>
|
||||
<string name="download_complete">Tamamlandı! Dosyayı açmak için dokunun.</string>
|
||||
<string name="download_complete">Tamamlandı!</string>
|
||||
<string name="hide_expired_entries_title">Süresi dolmuş girdileri gizle</string>
|
||||
<string name="hide_expired_entries_summary">Süresi dolmuş girdiler gizlenecek</string>
|
||||
<string name="warning_database_read_only">Veri tabanı değişikliklerini kaydetmek için dosya yazma erişimi ver</string>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<string name="entry_user_name">Ім’я користувача</string>
|
||||
<string name="error_arc4">Потокове шифрування Arcfour не підтримується.</string>
|
||||
<string name="error_can_not_handle_uri">Не вдалось обробити цей URI в KeePassDX.</string>
|
||||
<string name="error_file_not_create">Не вдалося створити нотатку:</string>
|
||||
<string name="error_file_not_create">Не вдалося створити нотатку</string>
|
||||
<string name="error_invalid_db">Неможливо прочитати базу даних.</string>
|
||||
<string name="error_invalid_path">Переконайтеся у правильності шляху.</string>
|
||||
<string name="error_no_name">Введіть назву.</string>
|
||||
@@ -152,7 +152,7 @@
|
||||
<string name="content_description_add_group">Додати групу</string>
|
||||
<string name="content_description_update_from_list">Оновити</string>
|
||||
<string name="keyboard">Клавіатура</string>
|
||||
<string name="compression_gzip">gzip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="compression">Стиснення</string>
|
||||
<string name="other">Інше</string>
|
||||
<string name="application_appearance">Застосунок</string>
|
||||
@@ -279,7 +279,7 @@
|
||||
<string name="kdf_Argon2">Argon2</string>
|
||||
<string name="kdf_AES">AES</string>
|
||||
<string name="encryption_chacha20">ChaCha20</string>
|
||||
<string name="download_complete">Готово! Торкніться, щоб відкрити файл.</string>
|
||||
<string name="download_complete">Готово!</string>
|
||||
<string name="download_finalization">Завершення…</string>
|
||||
<string name="download_progression">Виконується: %1$d%%</string>
|
||||
<string name="download_initialization">Ініціалізація…</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>
|
||||
@@ -423,7 +423,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="device_keyboard_setting_title">设备键盘设置</string>
|
||||
<string name="error_save_database">无法保存数据库。</string>
|
||||
<string name="menu_save_database">保存数据库</string>
|
||||
@@ -444,7 +444,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="contact">联系我们</string>
|
||||
|
||||
@@ -56,7 +56,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>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<string name="entry_user_name">Username</string>
|
||||
<string name="error_arc4">The Arcfour stream cipher is not supported.</string>
|
||||
<string name="error_can_not_handle_uri">Could not handle this URI in KeePassDX.</string>
|
||||
<string name="error_file_not_create">Could not create file:</string>
|
||||
<string name="error_file_not_create">Could not create file</string>
|
||||
<string name="error_invalid_db">Could not read the database.</string>
|
||||
<string name="error_invalid_path">Make sure the path is correct.</string>
|
||||
<string name="error_invalid_OTP">Invalid OTP secret.</string>
|
||||
@@ -257,6 +257,9 @@
|
||||
<string name="warning_empty_password">Continue without password unlocking protection?</string>
|
||||
<string name="warning_no_encryption_key">Continue without encryption key?</string>
|
||||
<string name="warning_permanently_delete_nodes">Permanently delete selected nodes?</string>
|
||||
<string name="warning_file_too_big">A KeePass database is supposed to contain only small utility files (such as PGP key files).\n\nYour database may get very large and reduce performance with this upload.</string>
|
||||
<string name="warning_replace_file">Uploading this file will replace the existing one.</string>
|
||||
<string name="warning_sure_add_file">Add the file anyway?</string>
|
||||
<string name="version_label">Version %1$s</string>
|
||||
<string name="build_label">Build %1$s</string>
|
||||
<string name="configure_biometric">Biometric prompt is supported, but not set up.</string>
|
||||
@@ -351,7 +354,7 @@
|
||||
<string name="other">Other</string>
|
||||
<string name="compression">Compression</string>
|
||||
<string name="compression_none">None</string>
|
||||
<string name="compression_gzip">gzip</string>
|
||||
<string name="compression_gzip">Gzip</string>
|
||||
<string name="recycle_bin">Recycle bin</string>
|
||||
<string name="keyboard">Keyboard</string>
|
||||
<string name="magic_keyboard_title">Magikeyboard</string>
|
||||
@@ -451,10 +454,11 @@
|
||||
<string name="download">Download</string>
|
||||
<string name="contribute">Contribute</string>
|
||||
<string name="download_attachment">Download %1$s</string>
|
||||
<string name="upload_attachment">Upload %1$s</string>
|
||||
<string name="download_initialization">Initializing…</string>
|
||||
<string name="download_progression">In progress: %1$d%%</string>
|
||||
<string name="download_finalization">Finalizing…</string>
|
||||
<string name="download_complete">Tap to open the file.</string>
|
||||
<string name="download_complete">Complete!</string>
|
||||
<string name="encryption_rijndael">Rijndael (AES)</string>
|
||||
<string name="encryption_twofish">Twofish</string>
|
||||
<string name="encryption_chacha20">ChaCha20</string>
|
||||
|
||||
@@ -1 +1 @@
|
||||
*
|
||||
* Add attachments
|
||||
|
||||
@@ -1 +1 @@
|
||||
*
|
||||
* Ajout des fichiers joints
|
||||
Reference in New Issue
Block a user