mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Entry activity with fragment
This commit is contained in:
@@ -32,28 +32,26 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
||||
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
||||
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
||||
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.NodeId
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikIME
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY
|
||||
@@ -61,11 +59,10 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@@ -75,23 +72,18 @@ class EntryActivity : LockingActivity() {
|
||||
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var titleIconView: ImageView? = null
|
||||
private var historyView: View? = null
|
||||
private var entryContentsView: EntryContentsView? = null
|
||||
private var entryProgress: ProgressBar? = null
|
||||
private var lockView: View? = null
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
private var mEntry: Entry? = null
|
||||
private var mEntryFragment: EntryFragment? = null
|
||||
|
||||
private var mIsHistory: Boolean = false
|
||||
private var mEntryLastVersion: Entry? = null
|
||||
private var mEntryHistoryPosition: Int = -1
|
||||
|
||||
private var mHideProtectedValue: Boolean = false
|
||||
private var mEntryHistory: EntryViewModel.EntryHistory? = null
|
||||
private val mEntryViewModel: EntryViewModel by viewModels()
|
||||
|
||||
private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null
|
||||
private var mAttachmentsToDownload: HashMap<Int, Attachment> = HashMap()
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var mFirstLaunchOfActivity: Boolean = false
|
||||
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
@@ -123,9 +115,6 @@ class EntryActivity : LockingActivity() {
|
||||
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
|
||||
titleIconView = findViewById(R.id.entry_icon)
|
||||
historyView = findViewById(R.id.history_container)
|
||||
entryContentsView = findViewById(R.id.entry_contents)
|
||||
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
|
||||
entryContentsView?.setAttachmentCipherKey(mDatabase)
|
||||
entryProgress = findViewById(R.id.entry_progress)
|
||||
lockView = findViewById(R.id.lock_button)
|
||||
|
||||
@@ -136,8 +125,6 @@ class EntryActivity : LockingActivity() {
|
||||
// Focus view to reinitialize timeout
|
||||
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this, mDatabase)
|
||||
|
||||
// Init the clipboard helper
|
||||
clipboardHelper = ClipboardHelper(this)
|
||||
mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true
|
||||
|
||||
// Init SAF manager
|
||||
@@ -146,6 +133,83 @@ class EntryActivity : LockingActivity() {
|
||||
// Init attachment service binder manager
|
||||
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
||||
|
||||
mEntryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) as? EntryFragment?
|
||||
if (mEntryFragment == null) {
|
||||
mEntryFragment = EntryFragment.getInstance()
|
||||
}
|
||||
// To show Fragment asynchronously
|
||||
lifecycleScope.launchWhenResumed {
|
||||
mEntryFragment?.let { fragment ->
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.entry_content, fragment, ENTRY_FRAGMENT_TAG)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
intent.getParcelableExtra<NodeId<UUID>?>(KEY_ENTRY)?.let { keyEntry ->
|
||||
// Remove extras to consume only one time
|
||||
intent.removeExtra(KEY_ENTRY)
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1)
|
||||
intent.removeExtra(KEY_ENTRY_HISTORY_POSITION)
|
||||
mEntryViewModel.selectEntry(keyEntry, historyPosition)
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
mEntryViewModel.entry.observe(this) { entryHistory ->
|
||||
mEntryHistory = entryHistory
|
||||
// Update last access time.
|
||||
entryHistory?.entry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (mFirstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.otpElement.observe(this) { otpElement ->
|
||||
when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
entryProgress?.visibility = View.GONE
|
||||
}
|
||||
// Refresh view if TOTP
|
||||
OtpType.TOTP -> {
|
||||
entryProgress?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected ->
|
||||
mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentSelected
|
||||
}
|
||||
}
|
||||
|
||||
mEntryViewModel.historySelected.observe(this) { historySelected ->
|
||||
historySelected.entry?.let { entry ->
|
||||
launch(this, entry, mReadOnly, historySelected.historyPosition)
|
||||
}
|
||||
}
|
||||
|
||||
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
||||
when (actionTask) {
|
||||
ACTION_DATABASE_RESTORE_ENTRY_HISTORY,
|
||||
@@ -174,59 +238,12 @@ class EntryActivity : LockingActivity() {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
mHideProtectedValue = PreferencesUtil.hideProtectedValue(this)
|
||||
|
||||
// Get Entry from UUID
|
||||
try {
|
||||
val keyEntry: NodeId<UUID>? = intent.getParcelableExtra(KEY_ENTRY)
|
||||
if (keyEntry != null) {
|
||||
mEntry = mDatabase?.getEntryById(keyEntry)
|
||||
mEntryLastVersion = mEntry
|
||||
}
|
||||
} catch (e: ClassCastException) {
|
||||
Log.e(TAG, "Unable to retrieve the entry key")
|
||||
}
|
||||
|
||||
val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition)
|
||||
mEntryHistoryPosition = historyPosition
|
||||
if (historyPosition >= 0) {
|
||||
mIsHistory = true
|
||||
mEntry = mEntry?.getHistory()?.get(historyPosition)
|
||||
}
|
||||
|
||||
if (mEntry == null) {
|
||||
Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Update last access time.
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
// Manage entry copy to start notification if allowed
|
||||
if (mFirstLaunchOfActivity) {
|
||||
// Manage entry to launch copying notification if allowed
|
||||
ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo)
|
||||
// Manage entry to populate Magikeyboard and launch keyboard notification if allowed
|
||||
if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAttachmentFileBinderManager?.apply {
|
||||
registerProgressTask()
|
||||
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
||||
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
||||
if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) {
|
||||
entryContentsView?.putAttachment(entryAttachmentState)
|
||||
mEntryFragment?.putAttachment(entryAttachmentState)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,6 +258,10 @@ class EntryActivity : LockingActivity() {
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun isHistory(): Boolean {
|
||||
return mEntryHistory?.historyPosition != -1
|
||||
}
|
||||
|
||||
private fun fillEntryDataInContentsView(entry: Entry) {
|
||||
|
||||
val entryInfo = entry.getEntryInfo(mDatabase)
|
||||
@@ -251,142 +272,26 @@ class EntryActivity : LockingActivity() {
|
||||
}
|
||||
|
||||
// Assign title text
|
||||
val entryTitle = entryInfo.title
|
||||
val entryTitle = if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id
|
||||
collapsingToolbarLayout?.title = entryTitle
|
||||
toolbar?.title = entryTitle
|
||||
|
||||
// Assign basic fields
|
||||
entryContentsView?.assignUserName(entryInfo.username) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.username,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(this)
|
||||
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(this@EntryActivity)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false)
|
||||
dialog.dismiss()
|
||||
fillEntryDataInContentsView(entry)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(entryInfo.password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
entryContentsView?.assignPassword(entryInfo.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entry.getOtpElement()?.let { otpElement ->
|
||||
entryContentsView?.assignOtp(otpElement, entryProgress) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.assignURL(entryInfo.url)
|
||||
entryContentsView?.assignNotes(entryInfo.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
entryContentsView?.clearExtraFields()
|
||||
entryInfo.customFields.forEach { field ->
|
||||
val label = field.name
|
||||
// OTP field is already managed in dedicated view
|
||||
// Template UUID must not be shown
|
||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD
|
||||
&& label != TemplateEngine.TEMPLATE_ENTRY_UUID) {
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField) {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(applicationContext, field.name))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entryContentsView?.setHiddenProtectedValue(mHideProtectedValue)
|
||||
|
||||
// Manage attachments
|
||||
entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem ->
|
||||
mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode ->
|
||||
mAttachmentsToDownload[requestCode] = attachmentItem
|
||||
}
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
entryContentsView?.assignCreationDate(entryInfo.creationTime)
|
||||
entryContentsView?.assignModificationDate(entryInfo.lastModificationTime)
|
||||
entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime)
|
||||
|
||||
// Manage history
|
||||
historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE
|
||||
if (mIsHistory) {
|
||||
// Assign history dedicated view
|
||||
historyView?.visibility = if (isHistory()) View.VISIBLE else View.GONE
|
||||
if (isHistory()) {
|
||||
val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
|
||||
collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK))
|
||||
taColorAccent.recycle()
|
||||
}
|
||||
entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position ->
|
||||
launch(this, historyItem, mReadOnly, position)
|
||||
}
|
||||
|
||||
// Assign special data
|
||||
entryContentsView?.assignUUID(entry.nodeId.id)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (requestCode) {
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE ->
|
||||
// Not directly get the entry from intent data but from database
|
||||
mEntry?.let {
|
||||
fillEntryDataInContentsView(it)
|
||||
}
|
||||
EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> {
|
||||
mEntryViewModel.reloadEntry()
|
||||
}
|
||||
}
|
||||
|
||||
mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri ->
|
||||
@@ -406,10 +311,10 @@ class EntryActivity : LockingActivity() {
|
||||
MenuUtil.contributionMenuInflater(inflater, menu)
|
||||
inflater.inflate(R.menu.entry, menu)
|
||||
inflater.inflate(R.menu.database, menu)
|
||||
if (mIsHistory && !mReadOnly) {
|
||||
if (isHistory() && !mReadOnly) {
|
||||
inflater.inflate(R.menu.entry_history, menu)
|
||||
}
|
||||
if (mIsHistory || mReadOnly) {
|
||||
if (isHistory() || mReadOnly) {
|
||||
menu.findItem(R.id.menu_save_database)?.isVisible = false
|
||||
menu.findItem(R.id.menu_edit)?.isVisible = false
|
||||
}
|
||||
@@ -421,10 +326,10 @@ class EntryActivity : LockingActivity() {
|
||||
gotoUrl?.apply {
|
||||
// In API >= 11 onCreateOptionsMenu may be called before onCreate completes
|
||||
// so mEntry may not be set
|
||||
if (mEntry == null) {
|
||||
if (mEntryHistory?.entry == null) {
|
||||
isVisible = false
|
||||
} else {
|
||||
if (mEntry?.url?.isEmpty() != false) {
|
||||
if (mEntryHistory?.entry?.url?.isEmpty() != false) {
|
||||
// disable button if url is not available
|
||||
isVisible = false
|
||||
}
|
||||
@@ -439,14 +344,12 @@ class EntryActivity : LockingActivity() {
|
||||
|
||||
private fun performedNextEducation(entryActivityEducation: EntryActivityEducation,
|
||||
menu: Menu) {
|
||||
val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView()
|
||||
val entryFieldCopyView: View? = mEntryFragment?.firstEntryFieldCopyView()
|
||||
val entryCopyEducationPerformed = entryFieldCopyView != null
|
||||
&& entryActivityEducation.checkAndPerformedEntryCopyEducation(
|
||||
entryFieldCopyView,
|
||||
{
|
||||
val appNameString = getString(R.string.app_name)
|
||||
clipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
mEntryFragment?.launchEntryCopyEducationAction()
|
||||
},
|
||||
{
|
||||
performedNextEducation(entryActivityEducation, menu)
|
||||
@@ -474,13 +377,13 @@ class EntryActivity : LockingActivity() {
|
||||
return true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
mEntry?.let {
|
||||
mEntryHistory?.entry?.let {
|
||||
EntryEditActivity.launch(this@EntryActivity, it)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.menu_goto_url -> {
|
||||
var url: String = mEntry?.url ?: ""
|
||||
var url: String = mEntryHistory?.entry?.url ?: ""
|
||||
|
||||
// Default http:// if no protocol specified
|
||||
if (!url.contains("://")) {
|
||||
@@ -491,18 +394,18 @@ class EntryActivity : LockingActivity() {
|
||||
return true
|
||||
}
|
||||
R.id.menu_restore_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mEntryHistory?.lastEntryVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
mEntryHistory?.historyPosition ?: -1,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
R.id.menu_delete_entry_history -> {
|
||||
mEntryLastVersion?.let { mainEntry ->
|
||||
mEntryHistory?.lastEntryVersion?.let { mainEntry ->
|
||||
mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(
|
||||
mainEntry,
|
||||
mEntryHistoryPosition,
|
||||
mEntryHistory?.historyPosition ?: -1,
|
||||
!mReadOnly && mAutoSaveEnable)
|
||||
}
|
||||
}
|
||||
@@ -526,7 +429,7 @@ class EntryActivity : LockingActivity() {
|
||||
override fun finish() {
|
||||
// Transit data in previous Activity after an update
|
||||
Intent().apply {
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry)
|
||||
putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntryHistory?.entry)
|
||||
setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this)
|
||||
}
|
||||
super.finish()
|
||||
@@ -540,6 +443,8 @@ class EntryActivity : LockingActivity() {
|
||||
const val KEY_ENTRY = "KEY_ENTRY"
|
||||
const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION"
|
||||
|
||||
const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG"
|
||||
|
||||
fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) {
|
||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||
val intent = Intent(activity, EntryActivity::class.java)
|
||||
|
||||
@@ -238,7 +238,7 @@ class EntryEditActivity : LockingActivity(),
|
||||
lifecycleScope.launchWhenResumed {
|
||||
entryEditFragment?.let { fragment ->
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.entry_edit_contents, fragment, ENTRY_EDIT_FRAGMENT_TAG)
|
||||
.replace(R.id.entry_edit_content, fragment, ENTRY_EDIT_FRAGMENT_TAG)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ import com.kunzisoft.keepass.database.element.template.TemplateField.STANDARD_UR
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField.STANDARD_USERNAME
|
||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.EntryInfo
|
||||
import com.kunzisoft.keepass.model.Field
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.view.*
|
||||
@@ -61,8 +64,6 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
private var mTemplate: Template = Template.STANDARD
|
||||
|
||||
private var mInflater: LayoutInflater? = null
|
||||
|
||||
private lateinit var rootView: View
|
||||
private lateinit var entryIconView: ImageView
|
||||
private lateinit var entryTitleView: EntryEditFieldView
|
||||
@@ -71,7 +72,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
|
||||
private lateinit var attachmentsContainerView: ViewGroup
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
private var mHideProtectedValue: Boolean = false
|
||||
@@ -96,9 +97,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry_edit_contents, container, false)
|
||||
|
||||
mInflater = inflater
|
||||
.inflate(R.layout.fragment_entry_edit, container, false)
|
||||
|
||||
fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext())
|
||||
|
||||
@@ -119,9 +118,9 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
|
||||
attachmentsAdapter.database = mDatabase
|
||||
attachmentsAdapter?.database = mDatabase
|
||||
//attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE)
|
||||
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
|
||||
attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize ->
|
||||
if (previousSize > 0 && newSize == 0) {
|
||||
attachmentsContainerView.collapse(true)
|
||||
} else if (previousSize == 0 && newSize == 1) {
|
||||
@@ -142,10 +141,10 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
rootView.resetAppTimeoutWhenViewFocusedOrChanged(requireContext(), mDatabase)
|
||||
|
||||
// Retrieve the new entry after an orientation change
|
||||
if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true)
|
||||
mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) {
|
||||
mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo
|
||||
if (arguments?.containsKey(KEY_ENTRY_INFO) == true)
|
||||
mEntryInfo = arguments?.getParcelable(KEY_ENTRY_INFO) ?: mEntryInfo
|
||||
else if (savedInstanceState?.containsKey(KEY_ENTRY_INFO) == true) {
|
||||
mEntryInfo = savedInstanceState.getParcelable(KEY_ENTRY_INFO) ?: mEntryInfo
|
||||
}
|
||||
|
||||
if (arguments?.containsKey(KEY_TEMPLATE) == true)
|
||||
@@ -164,7 +163,6 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
rootView.showByFading()
|
||||
|
||||
return rootView
|
||||
}
|
||||
|
||||
@@ -618,57 +616,59 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
*/
|
||||
|
||||
fun getAttachments(): List<Attachment> {
|
||||
return attachmentsAdapter.itemsList.map { it.attachment }
|
||||
return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf()
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: List<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 ->
|
||||
attachmentsAdapter?.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter?.onDeleteButtonClickListener = { item ->
|
||||
onDeleteItem.invoke(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun containsAttachment(): Boolean {
|
||||
return !attachmentsAdapter.isEmpty()
|
||||
return attachmentsAdapter?.isEmpty() != true
|
||||
}
|
||||
|
||||
fun containsAttachment(attachment: EntryAttachmentState): Boolean {
|
||||
return attachmentsAdapter.contains(attachment)
|
||||
return attachmentsAdapter?.contains(attachment) ?: false
|
||||
}
|
||||
|
||||
fun putAttachment(attachment: EntryAttachmentState,
|
||||
onPreviewLoaded: (() -> Unit)? = null) {
|
||||
attachmentsContainerView.visibility = View.VISIBLE
|
||||
attachmentsAdapter.putItem(attachment)
|
||||
attachmentsAdapter.onBinaryPreviewLoaded = {
|
||||
attachmentsAdapter?.putItem(attachment)
|
||||
attachmentsAdapter?.onBinaryPreviewLoaded = {
|
||||
onPreviewLoaded?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAttachment(attachment: EntryAttachmentState) {
|
||||
attachmentsAdapter.removeItem(attachment)
|
||||
attachmentsAdapter?.removeItem(attachment)
|
||||
}
|
||||
|
||||
fun clearAttachments() {
|
||||
attachmentsAdapter.clear()
|
||||
attachmentsAdapter?.clear()
|
||||
}
|
||||
|
||||
fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) {
|
||||
attachmentsListView.postDelayed({
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y
|
||||
?: 0F)
|
||||
)
|
||||
attachmentsAdapter?.indexOf(attachment)?.let { index ->
|
||||
position.invoke(attachmentsContainerView.y
|
||||
+ attachmentsListView.y
|
||||
+ (attachmentsListView.getChildAt(index)?.y
|
||||
?: 0F)
|
||||
)
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
populateEntryWithViews()
|
||||
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_ENTRY_INFO, mEntryInfo)
|
||||
outState.putParcelable(KEY_TEMPLATE, mTemplate)
|
||||
mTempDateTimeViewId?.let {
|
||||
outState.putInt(KEY_SELECTION_DATE_TIME_ID, it)
|
||||
@@ -678,10 +678,10 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
|
||||
const val KEY_TEMPLATE = "KEY_TEMPLATE"
|
||||
const val KEY_DATABASE = "KEY_DATABASE"
|
||||
const val KEY_SELECTION_DATE_TIME_ID = "KEY_SELECTION_DATE_TIME_ID"
|
||||
private const val KEY_ENTRY_INFO = "KEY_ENTRY_INFO"
|
||||
private const val KEY_TEMPLATE = "KEY_TEMPLATE"
|
||||
private const val KEY_DATABASE = "KEY_DATABASE"
|
||||
private const val KEY_SELECTION_DATE_TIME_ID = "KEY_SELECTION_DATE_TIME_ID"
|
||||
|
||||
private const val FIELD_USERNAME_TAG = "FIELD_USERNAME_TAG"
|
||||
private const val FIELD_PASSWORD_TAG = "FIELD_PASSWORD_TAG"
|
||||
@@ -695,7 +695,7 @@ class EntryEditFragment: DatabaseFragment() {
|
||||
//database: Database?): EntryEditFragment {
|
||||
return EntryEditFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
|
||||
putParcelable(KEY_ENTRY_INFO, entryInfo)
|
||||
putParcelable(KEY_TEMPLATE, template)
|
||||
// TODO Unique database key database.key
|
||||
putInt(KEY_DATABASE, 0)
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
package com.kunzisoft.keepass.activities.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
import com.kunzisoft.keepass.database.element.DateInstant
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.view.EntryFieldView
|
||||
import com.kunzisoft.keepass.view.showByFading
|
||||
import com.kunzisoft.keepass.viewmodels.EntryViewModel
|
||||
import java.util.*
|
||||
|
||||
class EntryFragment: DatabaseFragment() {
|
||||
|
||||
private lateinit var entryFieldsContainerView: View
|
||||
|
||||
private lateinit var userNameFieldView: EntryFieldView
|
||||
private lateinit var passwordFieldView: EntryFieldView
|
||||
private lateinit var otpFieldView: EntryFieldView
|
||||
private lateinit var urlFieldView: EntryFieldView
|
||||
private lateinit var notesFieldView: EntryFieldView
|
||||
|
||||
private lateinit var extraFieldsContainerView: View
|
||||
private lateinit var extraFieldsListView: ViewGroup
|
||||
|
||||
private lateinit var expiresDateView: TextView
|
||||
private lateinit var creationDateView: TextView
|
||||
private lateinit var modificationDateView: TextView
|
||||
private lateinit var expiresImageView: ImageView
|
||||
|
||||
private lateinit var attachmentsContainerView: View
|
||||
private lateinit var attachmentsListView: RecyclerView
|
||||
private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null
|
||||
|
||||
private lateinit var historyContainerView: View
|
||||
private lateinit var historyListView: RecyclerView
|
||||
private var historyAdapter: EntryHistoryAdapter? = null
|
||||
|
||||
private lateinit var uuidContainerView: View
|
||||
private lateinit var uuidView: TextView
|
||||
private lateinit var uuidReferenceView: TextView
|
||||
|
||||
private var mFontInVisibility: Boolean = false
|
||||
private var mHideProtectedValue: Boolean = false
|
||||
|
||||
private var mOtpRunnable: Runnable? = null
|
||||
private var mClipboardHelper: ClipboardHelper? = null
|
||||
|
||||
private val mEntryViewModel: EntryViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val rootView = inflater.cloneInContext(contextThemed)
|
||||
.inflate(R.layout.fragment_entry, container, false)
|
||||
|
||||
context?.let { context ->
|
||||
mClipboardHelper = ClipboardHelper(context)
|
||||
attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
attachmentsAdapter?.database = mDatabase
|
||||
historyAdapter = EntryHistoryAdapter(context)
|
||||
}
|
||||
|
||||
rootView.showByFading()
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
entryFieldsContainerView = view.findViewById(R.id.entry_fields_container)
|
||||
entryFieldsContainerView.visibility = View.GONE
|
||||
|
||||
userNameFieldView = view.findViewById(R.id.entry_user_name_field)
|
||||
userNameFieldView.setLabel(R.string.entry_user_name)
|
||||
|
||||
passwordFieldView = view.findViewById(R.id.entry_password_field)
|
||||
passwordFieldView.setLabel(R.string.entry_password)
|
||||
|
||||
otpFieldView = view.findViewById(R.id.entry_otp_field)
|
||||
otpFieldView.setLabel(R.string.entry_otp)
|
||||
|
||||
urlFieldView = view.findViewById(R.id.entry_url_field)
|
||||
urlFieldView.setLabel(R.string.entry_url)
|
||||
urlFieldView.setLinkAll()
|
||||
|
||||
notesFieldView = view.findViewById(R.id.entry_notes_field)
|
||||
notesFieldView.setLabel(R.string.entry_notes)
|
||||
notesFieldView.setAutoLink()
|
||||
|
||||
extraFieldsContainerView = view.findViewById(R.id.extra_fields_container)
|
||||
extraFieldsListView = view.findViewById(R.id.extra_fields_list)
|
||||
|
||||
attachmentsContainerView = view.findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = view.findViewById(R.id.entry_attachments_list)
|
||||
attachmentsListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = attachmentsAdapter
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
expiresDateView = view.findViewById(R.id.entry_expires_date)
|
||||
creationDateView = view.findViewById(R.id.entry_created)
|
||||
modificationDateView = view.findViewById(R.id.entry_modified)
|
||||
expiresImageView = view.findViewById(R.id.entry_expires_image)
|
||||
|
||||
historyContainerView = view.findViewById(R.id.entry_history_container)
|
||||
historyListView = view.findViewById(R.id.entry_history_list)
|
||||
historyListView.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||
adapter = historyAdapter
|
||||
}
|
||||
|
||||
uuidContainerView = view.findViewById(R.id.entry_UUID_container)
|
||||
uuidContainerView.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidView = view.findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = view.findViewById(R.id.entry_UUID_reference)
|
||||
|
||||
mEntryViewModel.entry.observe(viewLifecycleOwner) { entryHistory ->
|
||||
assignEntry(entryHistory?.entry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
context?.let { context ->
|
||||
mFontInVisibility = PreferencesUtil.fieldFontIsInVisibility(context)
|
||||
mHideProtectedValue = PreferencesUtil.hideProtectedValue(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun firstEntryFieldCopyView(): View? {
|
||||
return try {
|
||||
when {
|
||||
userNameFieldView.isVisible && userNameFieldView.copyButtonView.isVisible -> userNameFieldView.copyButtonView
|
||||
passwordFieldView.isVisible && passwordFieldView.copyButtonView.isVisible -> passwordFieldView.copyButtonView
|
||||
otpFieldView.isVisible && otpFieldView.copyButtonView.isVisible -> otpFieldView.copyButtonView
|
||||
urlFieldView.isVisible && urlFieldView.copyButtonView.isVisible -> urlFieldView.copyButtonView
|
||||
notesFieldView.isVisible && notesFieldView.copyButtonView.isVisible -> notesFieldView.copyButtonView
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun launchEntryCopyEducationAction() {
|
||||
val appNameString = getString(R.string.app_name)
|
||||
mClipboardHelper?.timeoutCopyToClipboard(appNameString,
|
||||
getString(R.string.copy_field, appNameString))
|
||||
}
|
||||
|
||||
private fun assignEntry(entry: Entry?) {
|
||||
context?.let { context ->
|
||||
val entryInfo = entry?.getEntryInfo(mDatabase)
|
||||
|
||||
entryInfo?.username?.let { userName ->
|
||||
assignUserName(userName) {
|
||||
mClipboardHelper?.timeoutCopyToClipboard(userName,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_user_name)))
|
||||
}
|
||||
}
|
||||
|
||||
val isFirstTimeAskAllowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(context)
|
||||
val allowCopyPasswordAndProtectedFields =
|
||||
PreferencesUtil.allowCopyPasswordAndProtectedFields(context)
|
||||
val showWarningClipboardDialogOnClickListener = View.OnClickListener {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(getString(R.string.allow_copy_password_warning) +
|
||||
"\n\n" +
|
||||
getString(R.string.clipboard_warning))
|
||||
.create().apply {
|
||||
setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true)
|
||||
dialog.dismiss()
|
||||
assignEntry(entry)
|
||||
}
|
||||
setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ ->
|
||||
PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false)
|
||||
dialog.dismiss()
|
||||
assignEntry(entry)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) {
|
||||
View.OnClickListener {
|
||||
entryInfo?.password?.let { password ->
|
||||
mClipboardHelper?.timeoutCopyToClipboard(password,
|
||||
getString(R.string.copy_field,
|
||||
getString(R.string.entry_password)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
showWarningClipboardDialogOnClickListener
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
assignPassword(entryInfo?.password,
|
||||
allowCopyPasswordAndProtectedFields,
|
||||
onPasswordCopyClickListener)
|
||||
|
||||
//Assign OTP field
|
||||
entry?.getOtpElement()?.let { otpElement ->
|
||||
assignOtp(otpElement) {
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
otpElement.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_otp))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assignURL(entryInfo?.url)
|
||||
assignNotes(entryInfo?.notes)
|
||||
|
||||
// Assign custom fields
|
||||
if (mDatabase?.allowEntryCustomFields() == true) {
|
||||
clearExtraFields()
|
||||
entryInfo?.customFields?.forEach { field ->
|
||||
val label = field.name
|
||||
// OTP field is already managed in dedicated view
|
||||
// Template UUID must not be shown
|
||||
if (label != OtpEntryFields.OTP_TOKEN_FIELD
|
||||
&& label != TemplateEngine.TEMPLATE_ENTRY_UUID) {
|
||||
val value = field.protectedValue
|
||||
val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields
|
||||
if (allowCopyProtectedField) {
|
||||
addExtraField(label, value, allowCopyProtectedField) {
|
||||
mClipboardHelper?.timeoutCopyToClipboard(
|
||||
value.toString(),
|
||||
getString(R.string.copy_field,
|
||||
TemplateField.getLocalizedName(context, field.name))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If dialog not already shown
|
||||
if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) {
|
||||
addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener)
|
||||
} else {
|
||||
addExtraField(label, value, allowCopyProtectedField, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHiddenProtectedValue(mHideProtectedValue)
|
||||
|
||||
// Manage attachments
|
||||
entryInfo?.attachments?.toSet()?.let { attachments ->
|
||||
assignAttachments(attachments)
|
||||
}
|
||||
|
||||
// Assign dates
|
||||
assignCreationDate(entryInfo?.creationTime)
|
||||
assignModificationDate(entryInfo?.lastModificationTime)
|
||||
setExpires(entryInfo?.expires ?: false, entryInfo?.expiryTime)
|
||||
|
||||
// Assign entry history
|
||||
assignHistory(entry?.getHistory())
|
||||
|
||||
// Assign special data
|
||||
assignUUID(entry?.nodeId?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignUserName(userName: String?,
|
||||
onClickListener: View.OnClickListener?) {
|
||||
userNameFieldView.apply {
|
||||
if (userName != null && userName.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(userName)
|
||||
applyFontVisibility(mFontInVisibility)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
assignCopyButtonClickListener(onClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignPassword(password: String?,
|
||||
allowCopyPassword: Boolean,
|
||||
onClickListener: View.OnClickListener?) {
|
||||
passwordFieldView.apply {
|
||||
if (password != null && password.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(password, true)
|
||||
applyFontVisibility(mFontInVisibility)
|
||||
activateCopyButton(allowCopyPassword)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
assignCopyButtonClickListener(onClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignOtp(otpElement: OtpElement?,
|
||||
onClickListener: View.OnClickListener) {
|
||||
otpFieldView.removeCallbacks(mOtpRunnable)
|
||||
|
||||
if (otpElement != null) {
|
||||
otpFieldView.visibility = View.VISIBLE
|
||||
|
||||
if (otpElement.token.isEmpty()) {
|
||||
otpFieldView.setValue(R.string.error_invalid_OTP)
|
||||
otpFieldView.activateCopyButton(false)
|
||||
otpFieldView.assignCopyButtonClickListener(null)
|
||||
} else {
|
||||
otpFieldView.setLabel(otpElement.type.name)
|
||||
otpFieldView.setValue(otpElement.token)
|
||||
otpFieldView.assignCopyButtonClickListener(onClickListener)
|
||||
|
||||
mOtpRunnable = Runnable {
|
||||
if (otpElement.shouldRefreshToken()) {
|
||||
otpFieldView.setValue(otpElement.token)
|
||||
}
|
||||
mEntryViewModel.onOtpElementUpdated(otpElement)
|
||||
otpFieldView.postDelayed(mOtpRunnable, 1000)
|
||||
}
|
||||
mEntryViewModel.onOtpElementUpdated(otpElement)
|
||||
otpFieldView.post(mOtpRunnable)
|
||||
}
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
otpFieldView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignURL(url: String?) {
|
||||
urlFieldView.apply {
|
||||
if (url != null && url.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(url)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignNotes(notes: String?) {
|
||||
notesFieldView.apply {
|
||||
if (notes != null && notes.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(notes)
|
||||
applyFontVisibility(mFontInVisibility)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setExpires(isExpires: Boolean, expiryTime: DateInstant?) {
|
||||
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
|
||||
expiresDateView.text = if (isExpires) {
|
||||
expiryTime?.getDateTimeString(resources)
|
||||
} else {
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assignCreationDate(date: DateInstant?) {
|
||||
creationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignModificationDate(date: DateInstant?) {
|
||||
modificationDateView.text = date?.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
private fun assignUUID(uuid: UUID?) {
|
||||
uuidView.text = uuid?.toString()
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
private fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
|
||||
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
|
||||
// Hidden style for custom fields
|
||||
extraFieldsListView.let {
|
||||
for (i in 0 until it.childCount) {
|
||||
val childCustomView = it.getChildAt(i)
|
||||
if (childCustomView is EntryFieldView)
|
||||
childCustomView.hiddenProtectedValue = hiddenProtectedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOrHideEntryFieldsContainer(hide: Boolean) {
|
||||
entryFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private fun showOrHideExtraFieldsContainer(hide: Boolean) {
|
||||
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun addExtraField(title: String,
|
||||
value: ProtectedString,
|
||||
allowCopy: Boolean,
|
||||
onCopyButtonClickListener: View.OnClickListener?) {
|
||||
context?.let { context ->
|
||||
extraFieldsListView.addView(EntryFieldView(context).apply {
|
||||
setLabel(TemplateField.getLocalizedName(context, title))
|
||||
setValue(value.toString(), value.isProtected)
|
||||
setAutoLink()
|
||||
activateCopyButton(allowCopy)
|
||||
assignCopyButtonClickListener(onCopyButtonClickListener)
|
||||
applyFontVisibility(mFontInVisibility)
|
||||
})
|
||||
|
||||
showOrHideExtraFieldsContainer(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearExtraFields() {
|
||||
extraFieldsListView.removeAllViews()
|
||||
showOrHideExtraFieldsContainer(true)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private fun showAttachments(show: Boolean) {
|
||||
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun assignAttachments(attachments: Set<Attachment>) {
|
||||
showAttachments(attachments.isNotEmpty())
|
||||
attachmentsAdapter?.assignItems(attachments.map { EntryAttachmentState(it, StreamDirection.DOWNLOAD) })
|
||||
attachmentsAdapter?.onItemClickListener = { item ->
|
||||
mEntryViewModel.onAttachmentSelected(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||
attachmentsAdapter?.putItem(attachmentToDownload)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
private fun assignHistory(history: ArrayList<Entry>?) {
|
||||
historyAdapter?.clear()
|
||||
history?.let {
|
||||
historyAdapter?.entryHistoryList?.addAll(history)
|
||||
}
|
||||
historyAdapter?.onItemClickListener = { item, position ->
|
||||
mEntryViewModel.onHistorySelected(item, position)
|
||||
}
|
||||
historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false)
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
historyAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getInstance(): EntryFragment {
|
||||
return EntryFragment().apply {
|
||||
arguments = Bundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,380 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePassDX.
|
||||
*
|
||||
* KeePassDX is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* KeePassDX is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.kunzisoft.keepass.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
|
||||
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
|
||||
import com.kunzisoft.keepass.database.element.Attachment
|
||||
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.security.ProtectedString
|
||||
import com.kunzisoft.keepass.utils.UuidUtil
|
||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||
import com.kunzisoft.keepass.model.StreamDirection
|
||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import com.kunzisoft.keepass.otp.OtpType
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import java.util.*
|
||||
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: LinearLayout(context, attrs, defStyle) {
|
||||
|
||||
private var fontInVisibility: Boolean = false
|
||||
|
||||
private val entryFieldsContainerView: View
|
||||
|
||||
private val userNameFieldView: EntryFieldView
|
||||
private val passwordFieldView: EntryFieldView
|
||||
private val otpFieldView: EntryFieldView
|
||||
private val urlFieldView: EntryFieldView
|
||||
private val notesFieldView: EntryFieldView
|
||||
|
||||
private var otpRunnable: Runnable? = null
|
||||
|
||||
private val extraFieldsContainerView: View
|
||||
private val extraFieldsListView: ViewGroup
|
||||
|
||||
private val expiresDateView: TextView
|
||||
private val creationDateView: TextView
|
||||
private val modificationDateView: TextView
|
||||
private val expiresImageView: ImageView
|
||||
|
||||
private val attachmentsContainerView: View
|
||||
private val attachmentsListView: RecyclerView
|
||||
private val attachmentsAdapter = EntryAttachmentsItemsAdapter(context)
|
||||
|
||||
private val historyContainerView: View
|
||||
private val historyListView: RecyclerView
|
||||
private val historyAdapter = EntryHistoryAdapter(context)
|
||||
|
||||
private val uuidContainerView: View
|
||||
private val uuidView: TextView
|
||||
private val uuidReferenceView: TextView
|
||||
|
||||
init {
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
|
||||
inflater?.inflate(R.layout.view_entry_contents, this)
|
||||
|
||||
entryFieldsContainerView = findViewById(R.id.entry_fields_container)
|
||||
entryFieldsContainerView.visibility = View.GONE
|
||||
|
||||
userNameFieldView = findViewById(R.id.entry_user_name_field)
|
||||
userNameFieldView.setLabel(R.string.entry_user_name)
|
||||
|
||||
passwordFieldView = findViewById(R.id.entry_password_field)
|
||||
passwordFieldView.setLabel(R.string.entry_password)
|
||||
|
||||
otpFieldView = findViewById(R.id.entry_otp_field)
|
||||
otpFieldView.setLabel(R.string.entry_otp)
|
||||
|
||||
urlFieldView = findViewById(R.id.entry_url_field)
|
||||
urlFieldView.setLabel(R.string.entry_url)
|
||||
urlFieldView.setLinkAll()
|
||||
|
||||
notesFieldView = findViewById(R.id.entry_notes_field)
|
||||
notesFieldView.setLabel(R.string.entry_notes)
|
||||
notesFieldView.setAutoLink()
|
||||
|
||||
extraFieldsContainerView = findViewById(R.id.extra_fields_container)
|
||||
extraFieldsListView = findViewById(R.id.extra_fields_list)
|
||||
|
||||
attachmentsContainerView = findViewById(R.id.entry_attachments_container)
|
||||
attachmentsListView = findViewById(R.id.entry_attachments_list)
|
||||
attachmentsListView?.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = attachmentsAdapter
|
||||
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
expiresDateView = findViewById(R.id.entry_expires_date)
|
||||
creationDateView = findViewById(R.id.entry_created)
|
||||
modificationDateView = findViewById(R.id.entry_modified)
|
||||
expiresImageView = findViewById(R.id.entry_expires_image)
|
||||
|
||||
historyContainerView = findViewById(R.id.entry_history_container)
|
||||
historyListView = findViewById(R.id.entry_history_list)
|
||||
historyListView?.apply {
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
|
||||
adapter = historyAdapter
|
||||
}
|
||||
|
||||
uuidContainerView = findViewById(R.id.entry_UUID_container)
|
||||
uuidContainerView?.apply {
|
||||
visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE
|
||||
}
|
||||
uuidView = findViewById(R.id.entry_UUID)
|
||||
uuidReferenceView = findViewById(R.id.entry_UUID_reference)
|
||||
}
|
||||
|
||||
fun applyFontVisibilityToFields(fontInVisibility: Boolean) {
|
||||
this.fontInVisibility = fontInVisibility
|
||||
}
|
||||
|
||||
fun firstEntryFieldCopyView(): View? {
|
||||
return when {
|
||||
userNameFieldView.isVisible && userNameFieldView.copyButtonView.isVisible -> userNameFieldView.copyButtonView
|
||||
passwordFieldView.isVisible && passwordFieldView.copyButtonView.isVisible -> passwordFieldView.copyButtonView
|
||||
otpFieldView.isVisible && otpFieldView.copyButtonView.isVisible -> otpFieldView.copyButtonView
|
||||
urlFieldView.isVisible && urlFieldView.copyButtonView.isVisible -> urlFieldView.copyButtonView
|
||||
notesFieldView.isVisible && notesFieldView.copyButtonView.isVisible -> notesFieldView.copyButtonView
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun assignUserName(userName: String?,
|
||||
onClickListener: OnClickListener?) {
|
||||
userNameFieldView.apply {
|
||||
if (userName != null && userName.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(userName)
|
||||
applyFontVisibility(fontInVisibility)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
assignCopyButtonClickListener(onClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignPassword(password: String?,
|
||||
allowCopyPassword: Boolean,
|
||||
onClickListener: OnClickListener?) {
|
||||
passwordFieldView.apply {
|
||||
if (password != null && password.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(password, true)
|
||||
applyFontVisibility(fontInVisibility)
|
||||
activateCopyButton(allowCopyPassword)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
assignCopyButtonClickListener(onClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignOtp(otpElement: OtpElement?,
|
||||
otpProgressView: ProgressBar?,
|
||||
onClickListener: OnClickListener) {
|
||||
otpFieldView.removeCallbacks(otpRunnable)
|
||||
|
||||
if (otpElement != null) {
|
||||
otpFieldView.visibility = View.VISIBLE
|
||||
|
||||
if (otpElement.token.isEmpty()) {
|
||||
otpFieldView.setValue(R.string.error_invalid_OTP)
|
||||
otpFieldView.activateCopyButton(false)
|
||||
otpFieldView.assignCopyButtonClickListener(null)
|
||||
} else {
|
||||
otpFieldView.setLabel(otpElement.type.name)
|
||||
otpFieldView.setValue(otpElement.token)
|
||||
otpFieldView.assignCopyButtonClickListener(onClickListener)
|
||||
|
||||
when (otpElement.type) {
|
||||
// Only add token if HOTP
|
||||
OtpType.HOTP -> {
|
||||
otpProgressView?.visibility = View.GONE
|
||||
}
|
||||
// Refresh view if TOTP
|
||||
OtpType.TOTP -> {
|
||||
otpProgressView?.apply {
|
||||
max = otpElement.period
|
||||
progress = otpElement.secondsRemaining
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
otpRunnable = Runnable {
|
||||
if (otpElement.shouldRefreshToken()) {
|
||||
otpFieldView.setValue(otpElement.token)
|
||||
}
|
||||
otpProgressView?.progress = otpElement.secondsRemaining
|
||||
otpFieldView.postDelayed(otpRunnable, 1000)
|
||||
}
|
||||
otpFieldView.post(otpRunnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
otpFieldView.visibility = View.GONE
|
||||
otpProgressView?.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun assignURL(url: String?) {
|
||||
urlFieldView.apply {
|
||||
if (url != null && url.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(url)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun assignNotes(notes: String?) {
|
||||
notesFieldView.apply {
|
||||
if (notes != null && notes.isNotEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
setValue(notes)
|
||||
applyFontVisibility(fontInVisibility)
|
||||
showOrHideEntryFieldsContainer(false)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setExpires(isExpires: Boolean, expiryTime: DateInstant) {
|
||||
expiresImageView.visibility = if (isExpires) View.VISIBLE else View.GONE
|
||||
expiresDateView.text = if (isExpires) {
|
||||
expiryTime.getDateTimeString(resources)
|
||||
} else {
|
||||
resources.getString(R.string.never)
|
||||
}
|
||||
}
|
||||
|
||||
fun assignCreationDate(date: DateInstant) {
|
||||
creationDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignModificationDate(date: DateInstant) {
|
||||
modificationDateView.text = date.getDateTimeString(resources)
|
||||
}
|
||||
|
||||
fun assignUUID(uuid: UUID) {
|
||||
uuidView.text = uuid.toString()
|
||||
uuidReferenceView.text = UuidUtil.toHexString(uuid)
|
||||
}
|
||||
|
||||
fun setHiddenProtectedValue(hiddenProtectedValue: Boolean) {
|
||||
passwordFieldView.hiddenProtectedValue = hiddenProtectedValue
|
||||
// Hidden style for custom fields
|
||||
extraFieldsListView.let {
|
||||
for (i in 0 until it.childCount) {
|
||||
val childCustomView = it.getChildAt(i)
|
||||
if (childCustomView is EntryFieldView)
|
||||
childCustomView.hiddenProtectedValue = hiddenProtectedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOrHideEntryFieldsContainer(hide: Boolean) {
|
||||
entryFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Extra Fields
|
||||
* -------------
|
||||
*/
|
||||
|
||||
private fun showOrHideExtraFieldsContainer(hide: Boolean) {
|
||||
extraFieldsContainerView.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
fun addExtraField(title: String,
|
||||
value: ProtectedString,
|
||||
allowCopy: Boolean,
|
||||
onCopyButtonClickListener: OnClickListener?) {
|
||||
|
||||
extraFieldsListView.addView(EntryFieldView(context).apply {
|
||||
setLabel(TemplateField.getLocalizedName(context, title))
|
||||
setValue(value.toString(), value.isProtected)
|
||||
setAutoLink()
|
||||
activateCopyButton(allowCopy)
|
||||
assignCopyButtonClickListener(onCopyButtonClickListener)
|
||||
applyFontVisibility(fontInVisibility)
|
||||
})
|
||||
|
||||
showOrHideExtraFieldsContainer(false)
|
||||
}
|
||||
|
||||
fun clearExtraFields() {
|
||||
extraFieldsListView.removeAllViews()
|
||||
showOrHideExtraFieldsContainer(true)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* Attachments
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun setAttachmentCipherKey(database: Database?) {
|
||||
attachmentsAdapter.database = database
|
||||
}
|
||||
|
||||
private fun showAttachments(show: Boolean) {
|
||||
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun assignAttachments(attachments: Set<Attachment>,
|
||||
streamDirection: StreamDirection,
|
||||
onAttachmentClicked: (attachment: Attachment) -> Unit) {
|
||||
showAttachments(attachments.isNotEmpty())
|
||||
attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) })
|
||||
attachmentsAdapter.onItemClickListener = { item ->
|
||||
onAttachmentClicked.invoke(item.attachment)
|
||||
}
|
||||
}
|
||||
|
||||
fun putAttachment(attachmentToDownload: EntryAttachmentState) {
|
||||
attachmentsAdapter.putItem(attachmentToDownload)
|
||||
}
|
||||
|
||||
/* -------------
|
||||
* History
|
||||
* -------------
|
||||
*/
|
||||
|
||||
fun assignHistory(history: ArrayList<Entry>, action: (historyItem: Entry, position: Int) -> Unit) {
|
||||
historyAdapter.clear()
|
||||
historyAdapter.entryHistoryList.addAll(history)
|
||||
historyAdapter.onItemClickListener = { item, position ->
|
||||
action.invoke(item, position)
|
||||
}
|
||||
historyContainerView.visibility = if (historyAdapter.entryHistoryList.isEmpty())
|
||||
View.GONE
|
||||
else
|
||||
View.VISIBLE
|
||||
historyAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun generateDefaultLayoutParams(): LayoutParams {
|
||||
return LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,7 @@ fun View.hideByFading() {
|
||||
|
||||
fun View.showByFading() {
|
||||
// Trick to keep the focus
|
||||
alpha = 0.01f
|
||||
alpha = 0.0001f
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.NodeId
|
||||
import com.kunzisoft.keepass.otp.OtpElement
|
||||
import java.util.*
|
||||
|
||||
|
||||
class EntryViewModel: ViewModel() {
|
||||
|
||||
private val mDatabase = Database.getInstance()
|
||||
|
||||
val entry : LiveData<EntryHistory> get() = _entry
|
||||
private val _entry = MutableLiveData<EntryHistory>()
|
||||
|
||||
val otpElement : LiveData<OtpElement> get() = _otpElement
|
||||
private val _otpElement = SingleLiveEvent<OtpElement>()
|
||||
|
||||
val attachmentSelected : LiveData<Attachment> get() = _attachmentSelected
|
||||
private val _attachmentSelected = SingleLiveEvent<Attachment>()
|
||||
|
||||
val historySelected : LiveData<EntryHistory> get() = _historySelected
|
||||
private val _historySelected = SingleLiveEvent<EntryHistory>()
|
||||
|
||||
fun selectEntry(nodeIdUUID: NodeId<UUID>?, historyPosition: Int) {
|
||||
if (nodeIdUUID != null) {
|
||||
val entryLastVersion = mDatabase.getEntryById(nodeIdUUID)
|
||||
var entry = entryLastVersion
|
||||
if (historyPosition > -1) {
|
||||
entry = entry?.getHistory()?.get(historyPosition)
|
||||
}
|
||||
entry?.touch(modified = false, touchParents = false)
|
||||
_entry.value = EntryHistory(entry, entryLastVersion, historyPosition)
|
||||
} else {
|
||||
_entry.value = EntryHistory(null, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadEntry() {
|
||||
_entry.value = entry.value
|
||||
}
|
||||
|
||||
fun onOtpElementUpdated(optElement: OtpElement) {
|
||||
_otpElement.value = optElement
|
||||
}
|
||||
|
||||
fun onAttachmentSelected(attachment: Attachment) {
|
||||
_attachmentSelected.value = attachment
|
||||
}
|
||||
|
||||
fun onHistorySelected(item: Entry, position: Int) {
|
||||
_historySelected.value = EntryHistory(item, null, position)
|
||||
}
|
||||
|
||||
// Custom data class to manage entry to retrieve and define is it's an history item (!= -1)
|
||||
data class EntryHistory(var entry: Entry?,
|
||||
var lastEntryVersion: Entry?,
|
||||
var historyPosition: Int = -1): Parcelable {
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readParcelable(Entry::class.java.classLoader),
|
||||
parcel.readParcelable(Entry::class.java.classLoader),
|
||||
parcel.readInt())
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(entry, flags)
|
||||
parcel.writeParcelable(lastEntryVersion, flags)
|
||||
parcel.writeInt(historyPosition)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<EntryHistory> {
|
||||
override fun createFromParcel(parcel: Parcel): EntryHistory {
|
||||
return EntryHistory(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<EntryHistory?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = EntryViewModel::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class SingleLiveEvent<T> : MutableLiveData<T>() {
|
||||
|
||||
private val mPending = AtomicBoolean(false)
|
||||
|
||||
@MainThread
|
||||
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
|
||||
|
||||
if (hasActiveObservers()) {
|
||||
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
|
||||
}
|
||||
|
||||
// Observe the internal MutableLiveData
|
||||
super.observe(owner, { t ->
|
||||
if (mPending.compareAndSet(true, false)) {
|
||||
observer.onChanged(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@MainThread
|
||||
override fun setValue(t: T?) {
|
||||
mPending.set(true)
|
||||
super.setValue(t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for cases where T is Void, to make calls cleaner.
|
||||
*/
|
||||
@MainThread
|
||||
fun call() {
|
||||
value = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = SingleLiveEvent::class.java.name
|
||||
}
|
||||
}
|
||||
@@ -113,8 +113,8 @@
|
||||
android:text="@string/entry_history"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.kunzisoft.keepass.view.EntryContentsView
|
||||
android:id="@+id/entry_contents"
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/entry_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/entry_edit_contents"
|
||||
android:id="@+id/entry_edit_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
|
||||
Reference in New Issue
Block a user