diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index c22f30326..059b8d002 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -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 = 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?>(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? = 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) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt index 20aca5270..679ba3184 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -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() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt index f56ae7c5c..cf5eb5837 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryEditFragment.kt @@ -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 { - return attachmentsAdapter.itemsList.map { it.attachment } + return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf() } fun assignAttachments(attachments: List, 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) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt new file mode 100644 index 000000000..9596296c2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt @@ -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) { + 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?) { + 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() + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt b/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt deleted file mode 100644 index ed50faf53..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/view/EntryContentsView.kt +++ /dev/null @@ -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 . - */ -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, - 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, 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) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 8100d9136..b477804c6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -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) diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt new file mode 100644 index 000000000..dbe687ba9 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryViewModel.kt @@ -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 get() = _entry + private val _entry = MutableLiveData() + + val otpElement : LiveData get() = _otpElement + private val _otpElement = SingleLiveEvent() + + val attachmentSelected : LiveData get() = _attachmentSelected + private val _attachmentSelected = SingleLiveEvent() + + val historySelected : LiveData get() = _historySelected + private val _historySelected = SingleLiveEvent() + + fun selectEntry(nodeIdUUID: NodeId?, 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 { + override fun createFromParcel(parcel: Parcel): EntryHistory { + return EntryHistory(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + companion object { + private val TAG = EntryViewModel::class.java.name + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/SingleLiveEvent.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/SingleLiveEvent.kt new file mode 100644 index 000000000..40d1e7d90 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/SingleLiveEvent.kt @@ -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 : MutableLiveData() { + + private val mPending = AtomicBoolean(false) + + @MainThread + override fun observe(owner: LifecycleOwner, observer: Observer) { + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_entry.xml b/app/src/main/res/layout/activity_entry.xml index e96c41ab3..15f1ab28c 100644 --- a/app/src/main/res/layout/activity_entry.xml +++ b/app/src/main/res/layout/activity_entry.xml @@ -113,8 +113,8 @@ android:text="@string/entry_history"/> -