mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
New clipboard manager #1343
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
KeePassDX(3.4.5)
|
||||
* Fix device credential unlocking #1344
|
||||
* New clipboard manager #1343
|
||||
* Keep screen on by default when viewing an entry
|
||||
|
||||
KeePassDX(3.4.4)
|
||||
|
||||
@@ -4,14 +4,14 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
ndkVersion "21.4.7075529"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 31
|
||||
targetSdkVersion 32
|
||||
versionCode = 114
|
||||
versionName = "3.4.5"
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.kunzisoft.keepass.view.TemplateView
|
||||
import com.kunzisoft.keepass.view.hideByFading
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
|
||||
class EntryFragment: DatabaseFragment() {
|
||||
|
||||
@@ -158,11 +157,9 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
setOnCopyActionClickListener { field ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
TemplateField.getLocalizedName(context, field.name),
|
||||
field.protectedValue.stringValue,
|
||||
getString(
|
||||
R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, field.name)
|
||||
)
|
||||
field.protectedValue.isProtected
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -251,8 +248,7 @@ class EntryFragment: DatabaseFragment() {
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString, appNameString)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -78,9 +78,11 @@ class PassphraseGeneratorFragment : DatabaseFragment() {
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passphraseCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.passphrase),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
wordCaseAdapter = ArrayAdapter(context,
|
||||
|
||||
@@ -99,9 +99,11 @@ class PasswordGeneratorFragment : DatabaseFragment() {
|
||||
View.VISIBLE else View.GONE
|
||||
val clipboardHelper = ClipboardHelper(context)
|
||||
passwordCopyView?.setOnClickListener {
|
||||
clipboardHelper.timeoutCopyToClipboard(passKeyView.passwordString,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
clipboardHelper.timeoutCopyToClipboard(
|
||||
getString(R.string.password),
|
||||
passKeyView.passwordString,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ package com.kunzisoft.keepass.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SortedList
|
||||
@@ -521,13 +521,14 @@ class NodesAdapter (private val context: Context,
|
||||
}
|
||||
holder?.otpContainer?.setOnClickListener {
|
||||
otpElement?.token?.let { token ->
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
mClipboardHelper.copyToClipboard(token)
|
||||
try {
|
||||
mClipboardHelper.copyToClipboard(
|
||||
TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN),
|
||||
token
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to copy the OTP token", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.exception
|
||||
|
||||
class ClipboardException(e: Exception) : Exception(e)
|
||||
@@ -33,6 +33,10 @@ class ClipboardEntryNotificationField : Parcelable {
|
||||
|
||||
private var id: NotificationFieldId = NotificationFieldId.UNKNOWN
|
||||
var label: String = ""
|
||||
val isSensitive: Boolean
|
||||
get() {
|
||||
return id == NotificationFieldId.PASSWORD
|
||||
}
|
||||
|
||||
val actionKey: String
|
||||
get() = getActionKey(id)
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper.NEVER
|
||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import java.util.*
|
||||
|
||||
class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
|
||||
@@ -75,7 +74,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
}
|
||||
ACTION_CLEAN_CLIPBOARD == intent.action -> {
|
||||
mTimerJob?.cancel()
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
stopNotificationAndSendLockIfNeeded()
|
||||
}
|
||||
else -> for (actionKey in ClipboardEntryNotificationField.allActionKeys) {
|
||||
@@ -153,7 +152,11 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
|
||||
try {
|
||||
var generatedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
|
||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||
clipboardHelper?.copyToClipboard(
|
||||
fieldToCopy.label,
|
||||
generatedValue,
|
||||
fieldToCopy.isSensitive
|
||||
)
|
||||
|
||||
val builder = buildNewNotification()
|
||||
.setSmallIcon(R.drawable.notification_ic_clipboard_key_24dp)
|
||||
@@ -186,13 +189,17 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
// New auto generated value
|
||||
if (generatedValue != newGeneratedValue) {
|
||||
generatedValue = newGeneratedValue
|
||||
clipboardHelper?.copyToClipboard(fieldToCopy.label, generatedValue)
|
||||
clipboardHelper?.copyToClipboard(
|
||||
fieldToCopy.label,
|
||||
generatedValue,
|
||||
fieldToCopy.isSensitive
|
||||
)
|
||||
}
|
||||
}) {
|
||||
stopNotificationAndSendLockIfNeeded()
|
||||
// Clean password only if no next field
|
||||
if (nextFields.size <= 0)
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
}
|
||||
} else {
|
||||
// No timer
|
||||
@@ -202,25 +209,15 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Clipboard can't be populate", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun cleanClipboard() {
|
||||
try {
|
||||
clipboardHelper?.cleanClipboard()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Clipboard can't be cleaned", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
cleanClipboard()
|
||||
|
||||
clipboardHelper?.cleanClipboard()
|
||||
super.onTaskRemoved(rootIntent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
cleanClipboard()
|
||||
clipboardHelper?.cleanClipboard()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,117 +24,117 @@ import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import android.text.SpannableString
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.exception.ClipboardException
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
class ClipboardHelper(private val context: Context) {
|
||||
class ClipboardHelper(context: Context) {
|
||||
|
||||
private var mAppContext = context.applicationContext
|
||||
private var mClipboardManager: ClipboardManager? = null
|
||||
|
||||
private val mTimer = Timer()
|
||||
|
||||
private fun getClipboardManager(): ClipboardManager? {
|
||||
if (mClipboardManager == null)
|
||||
mClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
mClipboardManager = mAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
|
||||
return mClipboardManager
|
||||
}
|
||||
|
||||
fun timeoutCopyToClipboard(text: String, toastString: String = "") {
|
||||
if (toastString.isNotEmpty())
|
||||
Toast.makeText(context, toastString, Toast.LENGTH_LONG).show()
|
||||
|
||||
fun timeoutCopyToClipboard(label: String, text: String, sensitive: Boolean = false) {
|
||||
try {
|
||||
copyToClipboard(text)
|
||||
} catch (e: ClipboardException) {
|
||||
copyToClipboard(label, text, sensitive)
|
||||
} catch (e: Exception) {
|
||||
showClipboardErrorDialog()
|
||||
return
|
||||
}
|
||||
|
||||
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(context)
|
||||
val clipboardTimeout = PreferencesUtil.getClipboardTimeout(mAppContext)
|
||||
if (clipboardTimeout > 0) {
|
||||
mTimer.schedule(ClearClipboardTask(context, text), clipboardTimeout)
|
||||
mTimer.schedule(ClearClipboardTask(text), clipboardTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getClipboard(context: Context): CharSequence {
|
||||
if (getClipboardManager()?.hasPrimaryClip() == true) {
|
||||
val data = getClipboardManager()?.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val text = data.getItemAt(0).coerceToText(context)
|
||||
if (text != null) {
|
||||
return text
|
||||
fun copyToClipboard(label: String, value: String, sensitive: Boolean = false) {
|
||||
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(DEFAULT_LABEL, value).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
description.extras = PersistableBundle().apply {
|
||||
putBoolean("android.content.extra.IS_SENSITIVE", sensitive)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (label.isNotEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
mAppContext,
|
||||
mAppContext.getString(
|
||||
R.string.copy_field,
|
||||
label
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun copyToClipboard(value: String) {
|
||||
copyToClipboard("", value)
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun copyToClipboard(label: String, value: String) {
|
||||
try {
|
||||
getClipboardManager()?.setPrimaryClip(ClipData.newPlainText(label, value))
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(ClipboardException::class)
|
||||
fun cleanClipboard(label: String = "") {
|
||||
fun cleanClipboard() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
getClipboardManager()?.clearPrimaryClip()
|
||||
} else {
|
||||
copyToClipboard(label, "")
|
||||
copyToClipboard(DEFAULT_LABEL, "")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw ClipboardException(e)
|
||||
Log.e("ClipboardHelper", "Unable to clean the clipboard", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Task which clears the clipboard, and sends a toast to the foreground.
|
||||
private inner class ClearClipboardTask (private val mCtx: Context,
|
||||
private val mClearText: String) : TimerTask() {
|
||||
private inner class ClearClipboardTask (private val mClearText: String) : TimerTask() {
|
||||
override fun run() {
|
||||
val currentClip = getClipboard(mCtx).toString()
|
||||
if (currentClip == mClearText) {
|
||||
try {
|
||||
cleanClipboard()
|
||||
R.string.clipboard_cleared
|
||||
} catch (e: ClipboardException) {
|
||||
R.string.clipboard_error_clear
|
||||
if (getClipboard(mAppContext).toString() == mClearText) {
|
||||
cleanClipboard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClipboard(context: Context): CharSequence {
|
||||
if (getClipboardManager()?.hasPrimaryClip() == true) {
|
||||
val data = getClipboardManager()?.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val text = data.getItemAt(0).coerceToText(context)
|
||||
if (text != null) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showClipboardErrorDialog() {
|
||||
val textDescription = context.getString(R.string.clipboard_error)
|
||||
val textDescription = mAppContext.getString(R.string.clipboard_error)
|
||||
val spannableString = SpannableString(textDescription)
|
||||
val textView = TextView(context).apply {
|
||||
val textView = TextView(mAppContext).apply {
|
||||
text = spannableString
|
||||
autoLinkMask = Activity.RESULT_OK
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
Linkify.addLinks(spannableString, Linkify.WEB_URLS)
|
||||
AlertDialog.Builder(context)
|
||||
AlertDialog.Builder(mAppContext)
|
||||
.setTitle(R.string.clipboard_error_title)
|
||||
.setView(textView)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LABEL = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
* Fix device credential unlocking #1344
|
||||
* New clipboard manager #1343
|
||||
* Keep screen on by default when viewing an entry
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
* Correction du déblocage par identifiants de l'appareil #1344
|
||||
* Nouveau gestionnaire de presse-papier #1343
|
||||
* Garder l'écran allumé par défaut lors d'une visualisation d'entrée
|
||||
|
||||
Reference in New Issue
Block a user