First pass to entry edit as ViewModel

This commit is contained in:
J-Jamet
2021-06-08 22:13:21 +02:00
parent c97f8c31ce
commit 82f5ab1446
4 changed files with 336 additions and 251 deletions

View File

@@ -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<EntryAttachmentState>()
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<EntryAttachmentState>()
// 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

View File

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

View File

@@ -40,6 +40,7 @@ class EntryInfo : NodeInfo {
var customFields: List<Field> = listOf()
var attachments: List<Attachment> = 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 {

View File

@@ -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<EntryInfo> get() = _entryInfo
private val _entryInfo = MutableLiveData<EntryInfo>()
val saveEntryRequested : LiveData<EntryInfo> get() = _requestSaveEntry
private val _requestSaveEntry = SingleLiveEvent<EntryInfo>()
val saveEntryResponded : LiveData<EntryInfo> get() = _responseSaveEntry
private val _responseSaveEntry = SingleLiveEvent<EntryInfo>()
val requestEntryInfoUpdate : LiveData<Void?> get() = _requestEntryInfoUpdate
private val _requestEntryInfoUpdate = SingleLiveEvent<Void?>()
val onEntryInfoUpdated : LiveData<EntryInfo> get() = _onEntryInfoUpdated
private val _onEntryInfoUpdated = SingleLiveEvent<EntryInfo>()
val templateChanged : LiveData<Template> get() = _template
private val _template = SingleLiveEvent<Template>()
val onTemplateChanged : LiveData<Template> get() = _onTemplateChanged
private val _onTemplateChanged = SingleLiveEvent<Template>()
fun setEntryInfo(entryInfo: EntryInfo?) {
_entryInfo.value = entryInfo
val requestIconSelection : LiveData<IconImage> get() = _requestIconSelection
private val _requestIconSelection = SingleLiveEvent<IconImage>()
val onIconSelected : LiveData<IconImage> get() = _onIconSelected
private val _onIconSelected = SingleLiveEvent<IconImage>()
val requestPasswordSelection : LiveData<Field> get() = _requestPasswordSelection
private val _requestPasswordSelection = SingleLiveEvent<Field>()
val onPasswordSelected : LiveData<Field> get() = _onPasswordSelected
private val _onPasswordSelected = SingleLiveEvent<Field>()
val requestCustomFieldEdition : LiveData<Field> get() = _requestCustomFieldEdition
private val _requestCustomFieldEdition = SingleLiveEvent<Field>()
val onCustomFieldEdited : LiveData<FieldEdition> get() = _onCustomFieldEdited
private val _onCustomFieldEdited = SingleLiveEvent<FieldEdition>()
val onCustomFieldError : LiveData<Void?> get() = _onCustomFieldError
private val _onCustomFieldError = SingleLiveEvent<Void?>()
val requestDateTimeSelection : LiveData<DateInstant> get() = _requestDateTimeSelection
private val _requestDateTimeSelection = SingleLiveEvent<DateInstant>()
val onDateSelected : LiveData<Date> get() = _onDateSelected
private val _onDateSelected = SingleLiveEvent<Date>()
val onTimeSelected : LiveData<Time> get() = _onTimeSelected
private val _onTimeSelected = SingleLiveEvent<Time>()
fun requestEntryInfoUpdate() {
_requestEntryInfoUpdate.call()
}
fun sendRequestSaveEntry() {
_requestSaveEntry.value = entryInfoLoaded.value
}
fun setResponseSaveEntry(entryInfo: EntryInfo?) {
_responseSaveEntry.value = entryInfo
fun updateEntryInfo(entryInfo: EntryInfo) {
_onEntryInfoUpdated.value = entryInfo
}
fun assignTemplate(template: Template) {
if (this.templateChanged.value != template) {
_template.value = template
if (this.onTemplateChanged.value != template) {
_onTemplateChanged.value = template
}
}
fun requestIconSelection(oldIconImage: IconImage) {
_requestIconSelection.value = oldIconImage
}
fun selectIcon(iconImage: IconImage) {
_onIconSelected.value = iconImage
}
fun requestPasswordSelection(passwordField: Field) {
_requestPasswordSelection.value = passwordField
}
fun selectPassword(passwordField: Field) {
_onPasswordSelected.value = passwordField
}
fun requestCustomFieldEdition(customField: Field) {
_requestCustomFieldEdition.value = customField
}
fun addCustomField(newField: Field) {
_onCustomFieldEdited.value = FieldEdition(null, newField)
}
fun editCustomField(oldField: Field, newField: Field) {
_onCustomFieldEdited.value = FieldEdition(oldField, newField)
}
fun removeCustomField(oldField: Field) {
_onCustomFieldEdited.value = FieldEdition(oldField, null)
}
fun showCustomFieldEditionError() {
_onCustomFieldError.call()
}
fun requestDateTimeSelection(dateInstant: DateInstant) {
_requestDateTimeSelection.value = dateInstant
}
fun selectDate(year: Int, month: Int, day: Int) {
_onDateSelected.value = Date(year, month, day)
}
fun selectTime(hours: Int, minutes: Int) {
_onTimeSelected.value = Time(hours, minutes)
}
data class Date(val year: Int, val month: Int, val day: Int)
data class Time(val hours: Int, val minutes: Int)
data class FieldEdition(val oldField: Field?, val newField: Field?)
companion object {
private val TAG = EntryEditViewModel::class.java.name
}