Merge branch 'feature/File_Attachment' into develop #189

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

View File

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

View File

@@ -45,8 +45,9 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
import com.kunzisoft.keepass.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

View File

@@ -22,6 +22,8 @@ import android.app.Activity
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -36,18 +38,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

View File

@@ -45,7 +45,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper.KEY_SEARCH_INFO
import com.kunzisoft.keepass.activities.helpers.OpenFileHelper
import com.kunzisoft.keepass.activities.helpers.SelectFileHelper
import com.kunzisoft.keepass.activities.selection.SpecialModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
@@ -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)
}
},
{}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,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

View File

@@ -23,36 +23,34 @@ import android.content.Context
import android.text.format.Formatter
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boolean)
: AnimatedItemsAdapter<EntryAttachment, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var onItemClickListener: ((item: EntryAttachment)->Unit)? = null
private val mDatabase = Database.getInstance()
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryBinariesViewHolder {
return EntryBinariesViewHolder(inflater.inflate(R.layout.item_attachment, parent, false))
}
override fun onBindViewHolder(holder: EntryBinariesViewHolder, position: Int) {
val entryAttachment = itemsList[position]
val entryAttachmentState = itemsList[position]
holder.itemView.visibility = View.VISIBLE
holder.binaryFileTitle.text = entryAttachment.name
holder.binaryFileTitle.text = entryAttachmentState.attachment.name
holder.binaryFileSize.text = Formatter.formatFileSize(context,
entryAttachment.binaryAttachment.length())
entryAttachmentState.attachment.binaryAttachment.length())
holder.binaryFileCompression.apply {
if (mDatabase.compressionAlgorithm == CompressionAlgorithm.GZip
|| entryAttachment.binaryAttachment.isCompressed == true) {
if (entryAttachmentState.attachment.binaryAttachment.isCompressed) {
text = CompressionAlgorithm.GZip.getName(context.resources)
visibility = View.VISIBLE
} else {
@@ -60,42 +58,60 @@ class EntryAttachmentsItemsAdapter(context: Context, private val editable: Boole
visibility = View.GONE
}
}
if (editable) {
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)
}

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
package com.kunzisoft.keepass.database.action.node
import android.content.Context
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node
@@ -40,16 +41,34 @@ class UpdateEntryRunnable constructor(
// WARNING : Re attribute parent removed in entry edit activity to save memory
mNewEntry.addParentFrom(mOldEntry)
// Build oldest attachments
val oldEntryAttachments = mOldEntry.getAttachments(database.binaryPool)
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 {

View File

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

View File

@@ -25,13 +25,12 @@ import android.net.Uri
import android.util.Log
import 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) {

View File

@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.database.element
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryPool
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -33,7 +34,6 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Field
import com.kunzisoft.keepass.otp.OtpElement
@@ -317,40 +317,38 @@ class Entry : Node, EntryVersionedInterface<Group> {
}
}
fun startToManageFieldReferences(db: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(db)
fun startToManageFieldReferences(database: DatabaseKDBX) {
entryKDBX?.startToManageFieldReferences(database)
}
fun stopToManageFieldReferences() {
entryKDBX?.stopToManageFieldReferences()
}
fun getAttachments(): ArrayList<EntryAttachment> {
val attachments = ArrayList<EntryAttachment>()
entryKDB?.binaryData?.let { binaryKDB ->
attachments.add(EntryAttachment(entryKDB?.binaryDescription ?: "", binaryKDB))
fun getAttachments(binaryPool: BinaryPool): 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 {

View File

@@ -17,10 +17,8 @@
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.security
package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import com.kunzisoft.keepass.stream.readBytes
@@ -30,7 +28,7 @@ import java.util.zip.GZIPOutputStream
class BinaryAttachment : Parcelable {
var isCompressed: Boolean? = null
var isCompressed: Boolean = false
private set
var isProtected: Boolean = false
private set
@@ -46,12 +44,12 @@ class BinaryAttachment : Parcelable {
* Empty protected binary
*/
constructor() {
this.isCompressed = null
this.isCompressed = false
this.isProtected = false
this.dataFile = null
}
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean? = null) {
constructor(dataFile: File, enableProtection: Boolean = false, compressed: Boolean = false) {
this.isCompressed = compressed
this.isProtected = enableProtection
this.dataFile = dataFile
@@ -59,7 +57,7 @@ class BinaryAttachment : Parcelable {
private constructor(parcel: Parcel) {
val compressedByte = parcel.readByte().toInt()
isCompressed = if (compressedByte == 2) null else compressedByte != 0
isCompressed = compressedByte != 0
isProtected = parcel.readByte().toInt() != 0
parcel.readString()?.let {
dataFile = File(it)
@@ -74,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)
}

View File

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

View File

@@ -28,6 +28,7 @@ import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.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

View File

@@ -29,8 +29,10 @@ import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfEngine
import com.kunzisoft.keepass.crypto.keyDerivation.KdfFactory
import com.kunzisoft.keepass.crypto.keyDerivation.KdfParameters
import com.kunzisoft.keepass.database.action.node.NodeHandler
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
@@ -40,12 +42,14 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.EncryptionAlgorithm
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.exception.UnknownKDF
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_3
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_32_4
import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.VariantDictionary
import org.w3c.dom.Node
import org.w3c.dom.Text
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
@@ -173,33 +177,51 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
fun changeBinaryCompression(oldCompression: CompressionAlgorithm,
newCompression: CompressionAlgorithm) {
binaryPool.doForEachBinary { key, binary ->
try {
when (oldCompression) {
CompressionAlgorithm.None -> {
when (newCompression) {
CompressionAlgorithm.None -> {
}
CompressionAlgorithm.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

View File

@@ -26,8 +26,10 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBInterface
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.Attachment
import java.util.*
import kotlin.collections.ArrayList
/**
* Structure containing information about one entry.
@@ -135,6 +137,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. */

View File

@@ -31,11 +31,13 @@ import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.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
}

View File

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

View File

@@ -26,6 +26,7 @@ import com.kunzisoft.keepass.crypto.StreamCipherFactory
import com.kunzisoft.keepass.crypto.engine.CipherEngine
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX.Companion.BASE_64_FLAG
@@ -35,7 +36,7 @@ import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.*
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX
@@ -49,12 +50,14 @@ import org.bouncycastle.crypto.StreamCipher
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.*
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.text.ParseException
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherInputStream
import kotlin.math.min
@@ -68,9 +71,6 @@ class DatabaseInputKDBX(cacheDirectory: File,
private var hashOfHeader: ByteArray? = null
private val unusedCacheFileName: String
get() = mDatabase.binaryPool.findUnusedKey().toString()
private var readNextNode = true
private val ctxGroups = Stack<GroupKDBX>()
private var ctxGroup: GroupKDBX? = null
@@ -233,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)

View File

@@ -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)

View File

@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.entry.EntryKDBX
import com.kunzisoft.keepass.database.element.group.GroupKDBX
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.node.NodeKDBXInterface
import com.kunzisoft.keepass.database.element.security.BinaryAttachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
@@ -55,7 +55,6 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.*
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
@@ -422,7 +421,6 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeBinary(binary : BinaryAttachment) {
val binaryLength = binary.length()
if (binaryLength > 0) {
if (binary.isProtected) {
xml.attribute(null, DatabaseKDBXXML.AttrProtected, DatabaseKDBXXML.ValTrue)
@@ -433,21 +431,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
xml.text(charArray, 0, charArray.size)
}
} else {
// Force binary compression from database (compression was harmonized during import)
if (mDatabaseKDBX.compressionAlgorithm === CompressionAlgorithm.GZip) {
if (binary.isCompressed) {
xml.attribute(null, DatabaseKDBXXML.AttrCompressed, DatabaseKDBXXML.ValTrue)
}
// Force decompression in this specific case
val binaryInputStream = if (mDatabaseKDBX.compressionAlgorithm == CompressionAlgorithm.None
&& binary.isCompressed == true) {
GZIPInputStream(binary.getInputDataStream())
} else {
binary.getInputDataStream()
}
// Write the XML
binaryInputStream.readBytes(BUFFER_SIZE_BYTES) { buffer ->
binary.getInputDataStream().readBytes(BUFFER_SIZE_BYTES) { buffer ->
val charArray = String(Base64.encode(buffer, BASE_64_FLAG)).toCharArray()
xml.text(charArray, 0, charArray.size)
}
@@ -459,10 +447,11 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private fun writeMetaBinaries() {
xml.startTag(null, DatabaseKDBXXML.ElemBinaries)
mDatabaseKDBX.binaryPool.doForEachBinary { key, binary ->
// Use indexes because necessarily in DatabaseV4 (binary header ref is the order)
mDatabaseKDBX.binaryPool.doForEachOrderedBinary { index, keyBinary ->
xml.startTag(null, DatabaseKDBXXML.ElemBinary)
xml.attribute(null, DatabaseKDBXXML.AttrId, key.toString())
writeBinary(binary)
xml.attribute(null, DatabaseKDBXXML.AttrId, index.toString())
writeBinary(keyBinary.binary)
xml.endTag(null, DatabaseKDBXXML.ElemBinary)
}
@@ -559,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>) {

View File

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

View File

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

View File

@@ -28,17 +28,24 @@ import android.os.IBinder
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachment
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.stream.readBytes
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.*
import java.io.BufferedInputStream
import java.util.*
import kotlin.collections.HashMap
import java.util.concurrent.CopyOnWriteArrayList
class AttachmentFileNotificationService: LockNotificationService() {
override val notificationId: Int = 10000
private val attachmentNotificationList = CopyOnWriteArrayList<AttachmentNotification>()
private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
@@ -51,33 +58,31 @@ class AttachmentFileNotificationService: LockNotificationService() {
fun addActionTaskListener(actionTaskListener: ActionTaskListener) {
mActionTaskListeners.add(actionTaskListener)
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(entry.key, attachment)
attachmentNotificationList.forEach {
it.attachmentFileAction?.listener = attachmentFileActionListener
}
}
}
})
}
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
entry.value.attachmentTask?.onUpdate = null
attachmentNotificationList.forEach {
it.attachmentFileAction?.listener = null
}
})
mActionTaskListeners.remove(actionTaskListener)
}
}
private val attachmentFileActionListener = object: AttachmentFileAction.AttachmentFileActionListener {
override fun onUpdate(attachmentNotification: AttachmentNotification) {
newNotification(attachmentNotification)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentAction(attachmentNotification.uri,
attachmentNotification.entryAttachmentState)
}
}
}
interface ActionTaskListener {
fun onAttachmentProgress(fileUri: Uri, attachment: EntryAttachment)
fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState)
}
override fun onBind(intent: Intent): IBinder? {
@@ -87,49 +92,29 @@ class AttachmentFileNotificationService: LockNotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val downloadFileUri: Uri? = if (intent?.hasExtra(DOWNLOAD_FILE_URI_KEY) == true) {
intent.getParcelableExtra(DOWNLOAD_FILE_URI_KEY)
val downloadFileUri: Uri? = if (intent?.hasExtra(FILE_URI_KEY) == true) {
intent.getParcelableExtra(FILE_URI_KEY)
} else null
when(intent?.action) {
ACTION_ATTACHMENT_FILE_START_UPLOAD -> {
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.UPLOAD)
}
ACTION_ATTACHMENT_FILE_START_DOWNLOAD -> {
if (downloadFileUri != null
&& intent.hasExtra(ATTACHMENT_KEY)) {
val nextNotificationId = (downloadFileUris.values.maxByOrNull { it.notificationId }
?.notificationId ?: notificationId) + 1
try {
intent.getParcelableExtra<EntryAttachment>(ATTACHMENT_KEY)?.let { entryAttachment ->
val attachmentNotification = AttachmentNotification(nextNotificationId, entryAttachment)
downloadFileUris[downloadFileUri] = attachmentNotification
mainScope.launch {
AttachmentFileActionClass(downloadFileUri,
attachmentNotification,
contentResolver).apply {
onUpdate = { uri, attachment, notificationIdAttach ->
newNotification(uri, attachment, notificationIdAttach)
mActionTaskListeners.forEach { actionListener ->
actionListener.onAttachmentProgress(downloadFileUri, attachment)
}
}
}.executeAction()
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to download $downloadFileUri", e)
}
}
actionUploadOrDownload(downloadFileUri,
intent,
StreamDirection.DOWNLOAD)
}
else -> {
if (downloadFileUri != null) {
downloadFileUris[downloadFileUri]?.notificationId?.let {
notificationManager?.cancel(it)
downloadFileUris.remove(downloadFileUri)
attachmentNotificationList.firstOrNull { it.uri == downloadFileUri }?.let { elementToRemove ->
notificationManager?.cancel(elementToRemove.notificationId)
attachmentNotificationList.remove(elementToRemove)
}
}
if (downloadFileUris.isEmpty()) {
if (attachmentNotificationList.isEmpty()) {
stopSelf()
}
}
@@ -138,25 +123,35 @@ class AttachmentFileNotificationService: LockNotificationService() {
return START_REDELIVER_INTENT
}
@Synchronized
fun checkCurrentAttachmentProgress() {
downloadFileUris.forEach(object : (Map.Entry<Uri, AttachmentNotification>) -> Unit {
override fun invoke(entry: Map.Entry<Uri, AttachmentNotification>) {
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>()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,9 +35,11 @@ import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.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)
}
/* -------------

View File

@@ -38,9 +38,11 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.icons.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()
}
/**

View File

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

View File

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

View File

@@ -31,7 +31,6 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Brugernavn</string>
<string name="error_arc4">Arcfour stream cipher er ikke understøttet.</string>
<string name="error_can_not_handle_uri">Kunne ikke håndtere URI i KeePassDX.</string>
<string name="error_file_not_create">Kunne ikke oprette fil:</string>
<string name="error_file_not_create">Kunne ikke oprette fil</string>
<string name="error_invalid_db">Kunne ikke læse databasen.</string>
<string name="error_invalid_path">Sørg for, at stien er korrekt.</string>
<string name="error_no_name">Indtast et navn.</string>
@@ -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>

View File

@@ -67,7 +67,7 @@
<string name="entry_user_name">Benutzername</string>
<string name="error_arc4">Die RC4/Arcfour-Stromverschlüsselung wird nicht unterstützt.</string>
<string name="error_can_not_handle_uri">KeePassDX kann diese URI-Adresse nicht verarbeiten.</string>
<string name="error_file_not_create">Konnte Datei nicht erstellen:</string>
<string name="error_file_not_create">Konnte Datei nicht erstellen</string>
<string name="error_invalid_db">Datenbank nicht lesbar.</string>
<string name="error_invalid_path">Sicherstellen, dass der Pfad korrekt ist.</string>
<string name="error_no_name">Namen eingeben.</string>
@@ -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>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Όνομα Χρήστη</string>
<string name="error_arc4">Η ροή κρυπτογράφησης Arcfour δεν υποστηρίζεται.</string>
<string name="error_can_not_handle_uri">Το KeePassDX δε μπορεί να χειριστεί αυτή τη διεύθυνση URI.</string>
<string name="error_file_not_create">Δεν ήταν δυνατή η δημιουργία αρχείου:</string>
<string name="error_file_not_create">Δεν ήταν δυνατή η δημιουργία αρχείου</string>
<string name="error_invalid_db">Δεν ήταν δυνατή η ανάγνωση της βάσης δεδομένων.</string>
<string name="error_invalid_path">Βεβαιωθείτε ότι η διαδρομή είναι σωστή.</string>
<string name="error_no_name">Εισαγάγετε ένα όνομα.</string>
@@ -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>

View File

@@ -58,7 +58,7 @@
<string name="entry_user_name">Nombre de usuario</string>
<string name="error_arc4">No se admite el cifrador de flujo Arcfour.</string>
<string name="error_can_not_handle_uri">KeePassDX no puede manejar este URI.</string>
<string name="error_file_not_create">No se pudo crear el archivo:</string>
<string name="error_file_not_create">No se pudo crear el archivo</string>
<string name="error_invalid_db">No se pudo leer la base de datos.</string>
<string name="error_invalid_path">Asegúrese de que la ruta sea correcta.</string>
<string name="error_no_name">Proporcione un nombre.</string>
@@ -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 &lt;strong&gt;mantener nuestra libertad&lt;/strong&gt;, &lt;strong&gt;corregir errores&lt;/strong&gt;, &lt;strong&gt;agregar características&lt;/strong&gt; y &lt;strong&gt;siempre estar activos&lt;/strong&gt;, contamos con tu &lt;strong&gt;contribución&lt;/strong&gt;.</string>
<string name="content_description_add_item">Añadir elemento</string>
<string name="download_complete">Descarga completa! Toca para abrir el archivo.</string>
<string name="download_complete">Descarga completa!</string>
<string name="download_finalization">Finalizando…</string>
<string name="download_progression">En progreso: %1$d%%</string>
<string name="download_initialization">Inicializando…</string>
@@ -407,7 +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>

View File

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

View File

@@ -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>

View File

@@ -65,7 +65,7 @@
<string name="entry_user_name">Nom dutilisateur</string>
<string name="error_arc4">Le chiffrement de flux Arcfour nest pas pris en charge.</string>
<string name="error_can_not_handle_uri">Impossible de gérer cette URI dans KeePassDX.</string>
<string name="error_file_not_create">Impossible de créer le fichier :</string>
<string name="error_file_not_create">Impossible de créer le fichier</string>
<string name="error_invalid_db">Impossible de lire la base de données.</string>
<string name="error_invalid_path">Vérifier la validité du chemin daccès.</string>
<string name="error_no_name">Saisir un nom.</string>
@@ -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 lappareil</string>
<string name="error_save_database">Impossible denregistrer la base de données.</string>
<string name="menu_save_database">Enregistrer la base de données</string>
@@ -456,7 +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>

View File

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

View File

@@ -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>

View File

@@ -60,7 +60,7 @@
<string name="entry_user_name">Felhasználónév</string>
<string name="error_arc4">Az Arcfour adatfolyam-titkosítás nem támogatott.</string>
<string name="error_can_not_handle_uri">Ez az URI nem kezelhető a KeePassDX-ben.</string>
<string name="error_file_not_create">Nem sikerült létrehozni a fájlt:</string>
<string name="error_file_not_create">Nem sikerült létrehozni a fájlt</string>
<string name="error_invalid_db">Az adatbázist nem lehet olvasni.</string>
<string name="error_invalid_path">Győződjön meg róla, hogy az útvonal helyes.</string>
<string name="error_no_name">Adjon meg egy nevet.</string>
@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -64,7 +64,7 @@
<string name="entry_user_name">Brukernavn</string>
<string name="error_arc4">Arcfour-strømchifferet støttes ikke.</string>
<string name="error_can_not_handle_uri">KeePassDX kan ikke håntere denne URI-en.</string>
<string name="error_file_not_create">Kunne ikke opprette fil:</string>
<string name="error_file_not_create">Kunne ikke opprette fil</string>
<string name="error_invalid_db">Ugyldig database eller fremmed hovednøkkel.</string>
<string name="error_invalid_path">Ugyldig sti.</string>
<string name="error_no_name">Et navn er påkrevd.</string>
@@ -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>

View File

@@ -59,7 +59,7 @@
<string name="entry_user_name">Gebruikersnaam</string>
<string name="error_arc4">De Arcfour stream-versleuteling wordt niet ondersteund.</string>
<string name="error_can_not_handle_uri">KeePassDX kan deze URI niet verwerken.</string>
<string name="error_file_not_create">Bestand is niet aangemaakt:</string>
<string name="error_file_not_create">Bestand is niet aangemaakt</string>
<string name="error_invalid_db">Kan database niet uitlezen.</string>
<string name="error_invalid_path">Zorg ervoor dat het pad juist is.</string>
<string name="error_no_name">Voer een naam in.</string>
@@ -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>

View File

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

View File

@@ -6,7 +6,7 @@
<string name="icon_pack_choose_title">ਆਈਕਾਨ ਪੈਕ</string>
<string name="style_choose_summary">ਐਪ ਵਿੱਚ ਵਰਤਿਆ ਥੀਮ</string>
<string name="style_choose_title">ਐਪ ਦਾ ਥੀਮ</string>
<string name="download_complete">ਪੂਰਾ ਹੋਇਆ! ਫ਼ਾਇਲ ਖੋਲ੍ਹਣ ਲਈ ਛੂਹੋ।</string>
<string name="download_complete">ਪੂਰਾ ਹੋਇਆ!</string>
<string name="download_finalization">…ਪੂਰਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
<string name="download_progression">ਜਾਰੀ ਹੈ: %1$d%%</string>
<string name="download_initialization">…ਸ਼ੁਰੂ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ</string>
@@ -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>

View File

@@ -55,7 +55,7 @@
<string name="entry_user_name">Nazwa użytkownika</string>
<string name="error_arc4">Strumieniowe szyfrowanie Arcfour nie jest wspierane.</string>
<string name="error_can_not_handle_uri">Nie można obsłużyć tego identyfikatora URI w KeePassDX.</string>
<string name="error_file_not_create">Nie można utworzyć pliku:</string>
<string name="error_file_not_create">Nie można utworzyć pliku</string>
<string name="error_invalid_db">Nie można odczytać bazy danych.</string>
<string name="error_invalid_path">Upewnij się, że ścieżka jest prawidłowa.</string>
<string name="error_no_name">Wpisz nazwę.</string>
@@ -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>

View File

@@ -56,7 +56,7 @@
<string name="entry_user_name">Nome de usuário</string>
<string name="error_arc4">A cifra de fluxo Arcfour não é suportada.</string>
<string name="error_can_not_handle_uri">Não pôde tratar esta URI no KeePassDX.</string>
<string name="error_file_not_create">Não foi possível criar o arquivo:</string>
<string name="error_file_not_create">Não foi possível criar o arquivo</string>
<string name="error_invalid_db">Falha ao ler o banco.</string>
<string name="error_invalid_path">Certifique-se de que o caminho está correto.</string>
<string name="error_no_name">Digite um nome.</string>
@@ -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>

View File

@@ -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 &lt;strong&gt;manter a liberdade&lt;/strong&gt;, &lt;strong&gt;solucionar bugs&lt;/strong&gt;, &lt;strong&gt;adicionar funções&lt;/strong&gt; e &lt;strong&gt;para sermos sempre ativoa&lt;/strong&gt;, contamos com sua &lt;strong&gt;contribuição&lt;/strong&gt;.</string>
<string name="biometric_unlock_enable_title">Desbloqueio por biométrico</string>
<string name="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>

View File

@@ -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>

View File

@@ -61,7 +61,7 @@
<string name="entry_user_name">Имя пользователя</string>
<string name="error_arc4">Потоковый шифр Arcfour не поддерживается.</string>
<string name="error_can_not_handle_uri">Невозможно обработать указанный 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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