Entry activity with fragment

This commit is contained in:
J-Jamet
2021-06-02 21:29:05 +02:00
parent a8d053e82a
commit 48e39d2ffa
12 changed files with 798 additions and 627 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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