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 cb1b2adeb..5dae14add 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -61,7 +61,6 @@ import com.kunzisoft.keepass.database.element.template.* import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.otp.OtpElement -import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.services.AttachmentFileNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService @@ -88,12 +87,6 @@ class EntryEditActivity : LockingActivity(), FileTooBigDialogFragment.ActionChooseListener, ReplaceFileDialogFragment.ActionChooseListener { - // Refs of an entry and group in database, are not modifiable - private var mEntry: Entry? = null - private var mParent: Group? = null - private var mIsTemplate: Boolean = false - private var mEntryTemplate: Template? = null - // Views private var coordinatorLayout: CoordinatorLayout? = null private var scrollView: NestedScrollView? = null @@ -104,14 +97,17 @@ class EntryEditActivity : LockingActivity(), private var lockView: View? = null private var loadingView: ProgressBar? = null - private var mEntryInfo: EntryInfo? = null + private var mParent : Group? = null + private var mEntry : Entry? = null + private var mIsTemplate: Boolean = false + private var mEntryTemplate: Template = Template.STANDARD + private var mTempAttachments = mutableListOf() private val mEntryEditViewModel: EntryEditViewModel by viewModels() // To manage attachments private var mExternalFileHelper: ExternalFileHelper? = null private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAllowMultipleAttachments: Boolean = false - private var mTempAttachments = ArrayList() // Education private var entryEditActivityEducation: EntryEditActivityEducation? = null @@ -184,10 +180,12 @@ class EntryEditActivity : LockingActivity(), // Define is current entry is a template (in direct template group) mIsTemplate = mDatabase?.entryIsTemplate(mEntry) ?: false - val templates = mDatabase?.getTemplates(mIsTemplate) + + // Default template mEntryTemplate = mEntry?.let { mDatabase?.getTemplate(it) - } ?: if (templates?.isNotEmpty() == true) Template.STANDARD else null + } ?: Template.STANDARD + // Decode the entry mEntry?.let { mEntry = mDatabase?.decodeEntryWithTemplateConfiguration(it) @@ -205,38 +203,42 @@ class EntryEditActivity : LockingActivity(), tempEntryInfo?.saveRegisterInfo(mDatabase, regInfo) } - mEntryEditViewModel.setEntryInfo(tempEntryInfo) - // Build fragment to manage entry modification entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? if (entryEditFragment == null) { - entryEditFragment = EntryEditFragment.getInstance() + entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mEntryTemplate) + } + + mEntryEditViewModel.requestIconSelection.observe(this) { iconImage -> + IconPickerActivity.launch(this@EntryEditActivity, iconImage) + } + mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> + if (dateInstant.type == DateInstant.Type.TIME) { + selectTime(dateInstant) + } else { + selectDate(dateInstant) + } + } + mEntryEditViewModel.requestPasswordSelection.observe(this) { passwordField -> + GeneratePasswordDialogFragment + .getInstance(passwordField) + .show(supportFragmentManager, "PasswordGeneratorFragment") + } + mEntryEditViewModel.requestCustomFieldEdition.observe(this) { field -> + editCustomField(field) + } + mEntryEditViewModel.onCustomFieldError.observe(this) { + coordinatorLayout?.let { + Snackbar.make(it, R.string.error_field_name_already_exists, Snackbar.LENGTH_LONG) + .asError() + .show() + } } entryEditFragment?.apply { - drawFactory = mDatabase?.iconDrawableFactory - onDateTimeClickListener = { dateInstant -> - if (dateInstant.type == DateInstant.Type.TIME) { - selectTime(dateInstant) - } else { - selectDate(dateInstant) - } - } - onPasswordGeneratorClickListener = { field -> - GeneratePasswordDialogFragment - .getInstance(field) - .show(supportFragmentManager, "PasswordGeneratorFragment") - } - // Add listener to the icon - onIconClickListener = { iconImage -> - IconPickerActivity.launch(this@EntryEditActivity, iconImage) - } onRemoveAttachment = { attachment -> mAttachmentFileBinderManager?.removeBinaryAttachment(attachment) removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD)) } - onEditCustomFieldClickListener = { field -> - editCustomField(field) - } } // To show Fragment asynchronously @@ -252,15 +254,16 @@ class EntryEditActivity : LockingActivity(), // Change template dynamically templateSelectorSpinner = findViewById(R.id.entry_edit_template_selector) templateSelectorSpinner?.apply { + val templates = mDatabase?.getTemplates(mIsTemplate) ?: listOf() // Build template selector - if (templates != null && templates.isNotEmpty()) { + if (templates.isNotEmpty()) { adapter = TemplatesSelectorAdapter(this@EntryEditActivity, mDatabase, templates) setSelection(templates.indexOf(mEntryTemplate)) onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val newTemplate = templates[position] + mEntryTemplate = templates[position] entryEditFragment?.apply { - mEntryEditViewModel.assignTemplate(newTemplate) + mEntryEditViewModel.assignTemplate(mEntryTemplate) } } override fun onNothingSelected(parent: AdapterView<*>?) {} @@ -270,80 +273,64 @@ class EntryEditActivity : LockingActivity(), } } - mEntryEditViewModel.entryInfoLoaded.observe(this) { entryInfo -> - mEntryInfo = entryInfo - } + // Build new entry from the entry info retrieved + mEntryEditViewModel.onEntryInfoUpdated.observe(this) { entryInfo -> - mEntryEditViewModel.saveEntryResponded.observe(this) { entryInfoSaved -> - // Get the temp entry - entryInfoSaved?.let { newEntryInfo -> - mEntry?.let { - // Create a clone - var newEntry = Entry(it) + mEntry?.let { oldEntry -> + // Create a clone + var newEntry = Entry(oldEntry) - // Do not save entry in upload progression - mTempAttachments.forEach { attachmentState -> - if (attachmentState.streamDirection == StreamDirection.UPLOAD) { - when (attachmentState.downloadState) { - AttachmentState.START, - AttachmentState.IN_PROGRESS, - AttachmentState.CANCELED, - AttachmentState.ERROR -> { - // Remove attachment not finished from info - newEntryInfo.attachments = newEntryInfo.attachments.toMutableList().apply { - remove(attachmentState.attachment) - } - } - else -> { + // Do not save entry in upload progression + mTempAttachments.forEach { attachmentState -> + if (attachmentState.streamDirection == StreamDirection.UPLOAD) { + when (attachmentState.downloadState) { + AttachmentState.START, + AttachmentState.IN_PROGRESS, + AttachmentState.CANCELED, + AttachmentState.ERROR -> { + // Remove attachment not finished from info + entryInfo.attachments = entryInfo.attachments.toMutableList().apply { + remove(attachmentState.attachment) } } - } - } - - // Build info - newEntry.setEntryInfo(mDatabase, newEntryInfo) - - // Encode entry properties for template - mEntryTemplate?.let { template -> - newEntry = mDatabase?.encodeEntryWithTemplateConfiguration(newEntry, template) - ?: newEntry - } - - // Delete temp attachment if not used - mTempAttachments.forEach { tempAttachmentState -> - val tempAttachment = tempAttachmentState.attachment - mDatabase?.attachmentPool?.let { binaryPool -> - if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { - mDatabase?.removeAttachmentIfNotUsed(tempAttachment) + else -> { } } } - - // Open a progress dialog and save entry - if (isEntryCreation()) { - mParent?.let { parent -> - mProgressDatabaseTaskProvider?.startDatabaseCreateEntry( - newEntry, - parent, - !mReadOnly && mAutoSaveEnable - ) - } - } else { - mEntry?.let { oldEntry -> - mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry( - oldEntry, - newEntry, - !mReadOnly && mAutoSaveEnable - ) - } - } } - } - } - // Retrieve temp attachments in case of deletion - if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) { - mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments + // Build info + newEntry.setEntryInfo(mDatabase, entryInfo) + + // Encode entry properties for template + newEntry = mDatabase?.encodeEntryWithTemplateConfiguration(newEntry, mEntryTemplate) + ?: newEntry + + // Delete temp attachment if not used + mTempAttachments.forEach { tempAttachmentState -> + val tempAttachment = tempAttachmentState.attachment + mDatabase?.attachmentPool?.let { binaryPool -> + if (!newEntry.getAttachments(binaryPool).contains(tempAttachment)) { + mDatabase?.removeAttachmentIfNotUsed(tempAttachment) + } + } + } + + // Open a progress dialog and save entry + mParent?.let { parent -> + mProgressDatabaseTaskProvider?.startDatabaseCreateEntry( + newEntry, + parent, + !mReadOnly && mAutoSaveEnable + ) + } ?: run { + mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry( + oldEntry, + newEntry, + !mReadOnly && mAutoSaveEnable + ) + } + } } // To retrieve attachment @@ -372,26 +359,25 @@ class EntryEditActivity : LockingActivity(), } if (newNodes.size == 1) { (newNodes[0] as? Entry?)?.let { entry -> - mEntry = entry EntrySelectionHelper.doSpecialAction(intent, { // Finish naturally - finishForEntryResult() + finishForEntryResult(actionTask, entry) }, { // Nothing when search retrieved }, { - entryValidatedForSave() + entryValidatedForSave(actionTask, entry) }, { - entryValidatedForKeyboardSelection(entry) + entryValidatedForKeyboardSelection(actionTask, entry) }, { _, _ -> entryValidatedForAutofillSelection(entry) }, { - entryValidatedForAutofillRegistration() + entryValidatedForAutofillRegistration(actionTask, entry) } ) } @@ -411,16 +397,12 @@ class EntryEditActivity : LockingActivity(), } } - private fun isEntryCreation() : Boolean { - return mParent != null - } - - private fun entryValidatedForSave() { + private fun entryValidatedForSave(actionTask: String, entry: Entry) { onValidateSpecialMode() - finishForEntryResult() + finishForEntryResult(actionTask, entry) } - private fun entryValidatedForKeyboardSelection(entry: Entry) { + private fun entryValidatedForKeyboardSelection(actionTask: String, entry: Entry) { // Populate Magikeyboard with entry mDatabase?.let { database -> populateKeyboardAndMoveAppToBackground(this, @@ -429,7 +411,7 @@ class EntryEditActivity : LockingActivity(), } onValidateSpecialMode() // Don't keep activity history for entry edition - finishForEntryResult() + finishForEntryResult(actionTask, entry) } private fun entryValidatedForAutofillSelection(entry: Entry) { @@ -444,9 +426,9 @@ class EntryEditActivity : LockingActivity(), onValidateSpecialMode() } - private fun entryValidatedForAutofillRegistration() { + private fun entryValidatedForAutofillRegistration(actionTask: String, entry: Entry) { onValidateSpecialMode() - finishForEntryResult() + finishForEntryResult(actionTask, entry) } override fun onResume() { @@ -474,7 +456,8 @@ class EntryEditActivity : LockingActivity(), getAttachmentViewPosition(entryAttachmentState) { scrollView?.smoothScrollTo(0, it.toInt()) } - } // Add in temp list + } + // Add in temp list mTempAttachments.add(entryAttachmentState) } AttachmentState.IN_PROGRESS -> { @@ -528,25 +511,16 @@ class EntryEditActivity : LockingActivity(), //} } - private fun showAddCustomFieldError() { - coordinatorLayout?.let { - Snackbar.make(it, R.string.error_field_name_already_exists, Snackbar.LENGTH_LONG).asError().show() - } - } - override fun onNewCustomFieldApproved(newField: Field) { - if (entryEditFragment?.putCustomField(newField) != true) - showAddCustomFieldError() + mEntryEditViewModel.addCustomField(newField) } override fun onEditCustomFieldApproved(oldField: Field, newField: Field) { - if (entryEditFragment?.replaceCustomField(oldField, newField) != true) { - showAddCustomFieldError() - } + mEntryEditViewModel.editCustomField(oldField, newField) } override fun onDeleteCustomFieldApproved(oldField: Field) { - entryEditFragment?.removeCustomField(oldField) + mEntryEditViewModel.removeCustomField(oldField) } /** @@ -596,7 +570,7 @@ class EntryEditActivity : LockingActivity(), super.onActivityResult(requestCode, resultCode, data) IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> - entryEditFragment?.setIcon(icon) + mEntryEditViewModel.selectIcon(icon) } mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> @@ -618,11 +592,8 @@ class EntryEditActivity : LockingActivity(), /** * Set up OTP (HOTP or TOTP) and add it as extra field */ - private fun setupOTP() { - // Retrieve the current otpElement if exists - // and open the dialog to set up the OTP - SetOTPDialogFragment.build(mEntryInfo?.otpModel) - .show(supportFragmentManager, "addOTPDialog") + private fun setupOtp() { + entryEditFragment?.setupOtp() } /** @@ -630,7 +601,7 @@ class EntryEditActivity : LockingActivity(), */ private fun saveEntry() { mAttachmentFileBinderManager?.stopUploadAllAttachments() - mEntryEditViewModel.sendRequestSaveEntry() + mEntryEditViewModel.requestEntryInfoUpdate() } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -705,7 +676,7 @@ class EntryEditActivity : LockingActivity(), && entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( setupOtpView, { - setupOTP() + setupOtp() } ) } @@ -724,7 +695,7 @@ class EntryEditActivity : LockingActivity(), return true } R.id.menu_add_otp -> { - setupOTP() + setupOtp() return true } android.R.id.home -> { @@ -736,23 +707,7 @@ class EntryEditActivity : LockingActivity(), } override fun onOtpCreated(otpElement: OtpElement) { - var titleOTP: String? = null - var usernameOTP: String? = null - // Build a temp entry to get title and username (by ref) - mEntryInfo?.let { entryInfo -> - val entryTemp = mDatabase?.createEntry() - entryTemp?.setEntryInfo(mDatabase, entryInfo) - mDatabase?.startManageEntry(entryTemp) - titleOTP = entryTemp?.title - usernameOTP = entryTemp?.username - mDatabase?.stopManageEntry(mEntry) - } - // Update the otp field with otpauth:// url - val otpField = OtpEntryFields.buildOtpField(otpElement, titleOTP, usernameOTP) - mEntry?.putExtraField(Field(otpField.name, otpField.protectedValue)) - entryEditFragment?.apply { - putCustomField(otpField) - } + entryEditFragment?.onOtpCreated(otpElement) } // Launch the date picker @@ -778,23 +733,16 @@ class EntryEditActivity : LockingActivity(), // To fix android 4.4 issue // https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice if (datePicker?.isShown == true) { - entryEditFragment?.setDate(year, month, day) + mEntryEditViewModel.selectDate(year, month, day) } } override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) { - entryEditFragment?.setTime(hours, minutes) - } - - override fun onSaveInstanceState(outState: Bundle) { - - outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments) - - super.onSaveInstanceState(outState) + mEntryEditViewModel.selectTime(hours, minutes) } override fun acceptPassword(passwordField: Field) { - entryEditFragment?.setPassword(passwordField) + mEntryEditViewModel.selectPassword(passwordField) entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post { performedNextEducation(it) } } @@ -832,17 +780,18 @@ class EntryEditActivity : LockingActivity(), } } - private fun finishForEntryResult() { + private fun finishForEntryResult(actionTask: String, entry: Entry) { // Assign entry callback as a result try { - mEntry?.let { entry -> - val bundle = Bundle() - val intentEntry = Intent() - bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry) - intentEntry.putExtras(bundle) - if (isEntryCreation()) { + val bundle = Bundle() + val intentEntry = Intent() + bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry) + intentEntry.putExtras(bundle) + when (actionTask) { + ACTION_DATABASE_CREATE_ENTRY_TASK -> { setResult(ADD_ENTRY_RESULT_CODE, intentEntry) - } else { + } + ACTION_DATABASE_UPDATE_ENTRY_TASK -> { setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry) } } @@ -861,9 +810,6 @@ class EntryEditActivity : LockingActivity(), const val KEY_ENTRY = "entry" const val KEY_PARENT = "parent" - // SaveInstanceState - const val TEMP_ATTACHMENTS = "TEMP_ATTACHMENTS" - // Keys for callback const val ADD_ENTRY_RESULT_CODE = 31 const val UPDATE_ENTRY_RESULT_CODE = 32 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 29e073361..98fa09cd2 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 @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.activities.fragments +import android.content.Context import android.graphics.Color import android.os.Build import android.os.Bundle @@ -34,6 +35,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.database.element.Attachment @@ -53,13 +55,14 @@ import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryInfo 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.view.* import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import org.joda.time.DateTime -class EntryEditFragment: DatabaseFragment() { +class EntryEditFragment: DatabaseFragment(), SetOTPDialogFragment.CreateOtpListener { private lateinit var rootView: View private lateinit var entryIconView: ImageView @@ -76,10 +79,6 @@ class EntryEditFragment: DatabaseFragment() { private var iconColor: Int = 0 var drawFactory: IconDrawableFactory? = null - var onIconClickListener: ((IconImage) -> Unit)? = null - var onPasswordGeneratorClickListener: ((Field) -> Unit)? = null - var onDateTimeClickListener: ((DateInstant) -> Unit)? = null - var onEditCustomFieldClickListener: ((Field) -> Unit)? = null var onRemoveAttachment: ((Attachment) -> Unit)? = null private val mEntryEditViewModel: EntryEditViewModel by activityViewModels() @@ -102,7 +101,7 @@ class EntryEditFragment: DatabaseFragment() { entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) entryIconView.setOnClickListener { - onIconClickListener?.invoke(mEntryInfo.icon) + mEntryEditViewModel.requestIconSelection(mEntryInfo.icon) } entryTitleView = rootView.findViewById(R.id.entry_edit_title) templateContainerView = rootView.findViewById(R.id.template_fields_container) @@ -139,24 +138,94 @@ class EntryEditFragment: DatabaseFragment() { rootView.resetAppTimeoutWhenViewFocusedOrChanged(requireContext(), mDatabase) + // Retrieve the new entry after an orientation change + 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) + mTemplate = arguments?.getParcelable(KEY_TEMPLATE) ?: mTemplate + else if (savedInstanceState?.containsKey(KEY_TEMPLATE) == true) { + mTemplate = savedInstanceState.getParcelable(KEY_TEMPLATE) ?: mTemplate + } + if (savedInstanceState?.containsKey(KEY_SELECTION_DATE_TIME_ID) == true) { mTempDateTimeViewId = savedInstanceState.getInt(KEY_SELECTION_DATE_TIME_ID) } - mEntryEditViewModel.entryInfoLoaded.observe(viewLifecycleOwner) { entryInfo -> - mEntryInfo = entryInfo - populateViewsWithEntry() - } + populateViewsWithEntry() - mEntryEditViewModel.templateChanged.observe(viewLifecycleOwner) { template -> + mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template -> mTemplate = template populateViewsWithEntry() rootView.showByFading() } - mEntryEditViewModel.saveEntryRequested.observe(viewLifecycleOwner) { + mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) { populateEntryWithViews() - mEntryEditViewModel.setResponseSaveEntry(mEntryInfo) + mEntryEditViewModel.updateEntryInfo(mEntryInfo) + } + + mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { + setIcon(it) + } + + mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { + setPassword(it) + } + + mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { + // Save the date + setCurrentDateTimeSelection { instant -> + val newDateInstant = DateInstant(DateTime(instant.date) + .withYear(it.year) + .withMonthOfYear(it.month + 1) + .withDayOfMonth(it.day) + .toDate(), instant.type) + if (instant.type == DateInstant.Type.DATE_TIME) { + val instantTime = DateInstant(instant.date, DateInstant.Type.TIME) + // Trick to recall selection with time + mEntryEditViewModel.requestDateTimeSelection(instantTime) + } + newDateInstant + } + } + + mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { + // Save the time + setCurrentDateTimeSelection { instant -> + DateInstant(DateTime(instant.date) + .withHourOfDay(it.hours) + .withMinuteOfHour(it.minutes) + .toDate(), instant.type) + } + } + + mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction -> + // Field to add + if (fieldAction.oldField == null) { + fieldAction.newField?.let { + if (!putCustomField(it)) { + mEntryEditViewModel.showCustomFieldEditionError() + } + } + } + // Field to replace + fieldAction.oldField?.let { + fieldAction.newField?.let { + if (!replaceCustomField(fieldAction.oldField, fieldAction.newField)) { + mEntryEditViewModel.showCustomFieldEditionError() + } + } + } + // Field to remove + if (fieldAction.newField == null) { + fieldAction.oldField?.let { + removeCustomField(it) + } + } } assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment -> @@ -167,15 +236,17 @@ class EntryEditFragment: DatabaseFragment() { return rootView } + override fun onAttach(context: Context) { + super.onAttach(context) + + drawFactory = mDatabase?.iconDrawableFactory + } + override fun onDetach() { super.onDetach() drawFactory = null - onDateTimeClickListener = null - onPasswordGeneratorClickListener = null - onIconClickListener = null onRemoveAttachment = null - onEditCustomFieldClickListener = null } fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean { @@ -346,12 +417,12 @@ class EntryEditFragment: DatabaseFragment() { } TemplateAttributeAction.CUSTOM_EDITION -> { setOnActionClickListener({ - onEditCustomFieldClickListener?.invoke(field) + mEntryEditViewModel.requestCustomFieldEdition(field) }, R.drawable.ic_more_white_24dp) } TemplateAttributeAction.PASSWORD_GENERATION -> { setOnActionClickListener({ - onPasswordGeneratorClickListener?.invoke(field) + mEntryEditViewModel.requestPasswordSelection(field) }, R.drawable.ic_generate_password_white_24dp) } } @@ -387,7 +458,7 @@ class EntryEditFragment: DatabaseFragment() { } setOnDateClickListener = { dateInstant -> mTempDateTimeViewId = id - onDateTimeClickListener?.invoke(dateInstant) + mEntryEditViewModel.requestDateTimeSelection(dateInstant) } } } @@ -450,12 +521,12 @@ class EntryEditFragment: DatabaseFragment() { ?: customFieldsContainerView.findViewById(viewId) } - fun setIcon(iconImage: IconImage) { + private fun setIcon(iconImage: IconImage) { mEntryInfo.icon = iconImage drawFactory?.assignDatabaseIcon(entryIconView, iconImage, iconColor) } - fun setPassword(passwordField: Field) { + private fun setPassword(passwordField: Field) { val passwordValue = passwordField.protectedValue.stringValue mEntryInfo.password = passwordValue val passwordView = getFieldViewByField(passwordField) @@ -475,33 +546,6 @@ class EntryEditFragment: DatabaseFragment() { } } - fun setDate(year: Int, month: Int, day: Int) { - // Save the date - setCurrentDateTimeSelection { instant -> - val newDateInstant = DateInstant(DateTime(instant.date) - .withYear(year) - .withMonthOfYear(month + 1) - .withDayOfMonth(day) - .toDate(), instant.type) - if (instant.type == DateInstant.Type.DATE_TIME) { - val instantTime = DateInstant(instant.date, DateInstant.Type.TIME) - // Trick to recall selection with time - onDateTimeClickListener?.invoke(instantTime) - } - newDateInstant - } - } - - fun setTime(hours: Int, minutes: Int) { - // Save the time - setCurrentDateTimeSelection { instant -> - DateInstant(DateTime(instant.date) - .withHourOfDay(hours) - .withMinuteOfHour(minutes) - .toDate(), instant.type) - } - } - /* ------------- * Custom Fields * ------------- @@ -539,7 +583,7 @@ class EntryEditFragment: DatabaseFragment() { /** * Update a custom field or create a new one if doesn't exists, the old value is lost */ - fun putCustomField(customField: Field): Boolean { + private fun putCustomField(customField: Field): Boolean { return if (!isStandardFieldName(customField.name)) { customFieldsContainerView.visibility = View.VISIBLE if (indexCustomFieldIdByName(customField.name) >= 0) { @@ -561,7 +605,7 @@ class EntryEditFragment: DatabaseFragment() { /** * Update a custom field and keep the old value */ - fun replaceCustomField(oldField: Field, newField: Field): Boolean { + private fun replaceCustomField(oldField: Field, newField: Field): Boolean { if (!isStandardFieldName(newField.name)) { customFieldIdByName(oldField.name)?.viewId?.let { viewId -> customFieldsContainerView.findViewById(viewId)?.let { viewToReplace -> @@ -590,7 +634,7 @@ class EntryEditFragment: DatabaseFragment() { return false } - fun removeCustomField(oldCustomField: Field) { + private fun removeCustomField(oldCustomField: Field) { val indexOldField = indexCustomFieldIdByName(oldCustomField.name) if (indexOldField > 0) { mCustomFieldIds[indexOldField].viewId.let { viewId -> @@ -600,6 +644,19 @@ class EntryEditFragment: DatabaseFragment() { } } + fun setupOtp() { + // Retrieve the current otpElement if exists + // and open the dialog to set up the OTP + SetOTPDialogFragment.build(mEntryInfo.otpModel) + .show(parentFragmentManager, "addOTPDialog") + } + + override fun onOtpCreated(otpElement: OtpElement) { + // Update the otp field with otpauth:// url + val otpField = OtpEntryFields.buildOtpField(otpElement, mEntryInfo.title, mEntryInfo.username) + putCustomField(Field(otpField.name, otpField.protectedValue)) + } + /* ------------- * Attachments * ------------- @@ -658,6 +715,8 @@ class EntryEditFragment: DatabaseFragment() { override fun onSaveInstanceState(outState: Bundle) { populateEntryWithViews() + outState.putParcelable(KEY_ENTRY_INFO, mEntryInfo) + outState.putParcelable(KEY_TEMPLATE, mTemplate) mTempDateTimeViewId?.let { outState.putInt(KEY_SELECTION_DATE_TIME_ID, it) } @@ -666,6 +725,8 @@ class EntryEditFragment: DatabaseFragment() { } companion object { + private const val KEY_ENTRY_INFO = "KEY_ENTRY_INFO" + private const val KEY_TEMPLATE = "KEY_TEMPLATE" private const val KEY_SELECTION_DATE_TIME_ID = "KEY_SELECTION_DATE_TIME_ID" private const val FIELD_USERNAME_TAG = "FIELD_USERNAME_TAG" @@ -675,9 +736,13 @@ class EntryEditFragment: DatabaseFragment() { private const val FIELD_NOTES_TAG = "FIELD_NOTES_TAG" private const val FIELD_CUSTOM_TAG = "FIELD_CUSTOM_TAG" - fun getInstance(): EntryEditFragment { + fun getInstance(entryInfo: EntryInfo?, + template: Template?): EntryEditFragment { return EntryEditFragment().apply { - arguments = Bundle().apply {} + arguments = Bundle().apply { + putParcelable(KEY_ENTRY_INFO, entryInfo) + putParcelable(KEY_TEMPLATE, template) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt index 65c5291af..3ad95f254 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/EntryInfo.kt @@ -40,6 +40,7 @@ class EntryInfo : NodeInfo { var customFields: List = listOf() var attachments: List = listOf() var otpModel: OtpModel? = null + var isTemplate: Boolean = false constructor() : super() @@ -52,6 +53,7 @@ class EntryInfo : NodeInfo { parcel.readList(customFields, Field::class.java.classLoader) parcel.readList(attachments, Attachment::class.java.classLoader) otpModel = parcel.readParcelable(OtpModel::class.java.classLoader) ?: otpModel + isTemplate = parcel.readByte().toInt() != 0 } override fun describeContents(): Int { @@ -68,6 +70,7 @@ class EntryInfo : NodeInfo { parcel.writeArray(customFields.toTypedArray()) parcel.writeArray(attachments.toTypedArray()) parcel.writeParcelable(otpModel, flags) + parcel.writeByte((if (isTemplate) 1 else 0).toByte()) } fun containsCustomFieldsProtected(): Boolean { diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt index 1fcf4626b..22979a626 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -1,43 +1,114 @@ package com.kunzisoft.keepass.viewmodels import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.Field +import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.model.EntryInfo class EntryEditViewModel: ViewModel() { - - val entryInfoLoaded : LiveData get() = _entryInfo - private val _entryInfo = MutableLiveData() - val saveEntryRequested : LiveData get() = _requestSaveEntry - private val _requestSaveEntry = SingleLiveEvent() - val saveEntryResponded : LiveData get() = _responseSaveEntry - private val _responseSaveEntry = SingleLiveEvent() + val requestEntryInfoUpdate : LiveData get() = _requestEntryInfoUpdate + private val _requestEntryInfoUpdate = SingleLiveEvent() + val onEntryInfoUpdated : LiveData get() = _onEntryInfoUpdated + private val _onEntryInfoUpdated = SingleLiveEvent() - val templateChanged : LiveData