mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
961 lines
39 KiB
Kotlin
961 lines
39 KiB
Kotlin
/*
|
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
*
|
|
* This file is part of KeePassDX.
|
|
*
|
|
* KeePassDX is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* KeePassDX is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package com.kunzisoft.keepass.activities
|
|
|
|
import android.app.Activity
|
|
import android.app.DatePickerDialog
|
|
import android.app.TimePickerDialog
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.util.Log
|
|
import android.view.Menu
|
|
import android.view.MenuItem
|
|
import android.view.View
|
|
import android.widget.DatePicker
|
|
import android.widget.TimePicker
|
|
import androidx.annotation.RequiresApi
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
import androidx.core.view.isVisible
|
|
import androidx.core.widget.NestedScrollView
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import com.kunzisoft.keepass.R
|
|
import com.kunzisoft.keepass.activities.dialogs.*
|
|
import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Companion.MAX_WARNING_BINARY_FILE
|
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
|
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
|
import com.kunzisoft.keepass.activities.lock.LockingActivity
|
|
import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged
|
|
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
import com.kunzisoft.keepass.database.element.*
|
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
|
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
|
|
import com.kunzisoft.keepass.database.element.node.Node
|
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
|
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
|
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK
|
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
|
import com.kunzisoft.keepass.utils.UriUtil
|
|
import com.kunzisoft.keepass.view.ToolbarAction
|
|
import com.kunzisoft.keepass.view.asError
|
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
|
import com.kunzisoft.keepass.view.updateLockPaddingLeft
|
|
import org.joda.time.DateTime
|
|
import java.util.*
|
|
import kotlin.collections.ArrayList
|
|
|
|
class EntryEditActivity : LockingActivity(),
|
|
EntryCustomFieldDialogFragment.EntryCustomFieldListener,
|
|
GeneratePasswordDialogFragment.GeneratePasswordListener,
|
|
SetOTPDialogFragment.CreateOtpListener,
|
|
DatePickerDialog.OnDateSetListener,
|
|
TimePickerDialog.OnTimeSetListener,
|
|
FileTooBigDialogFragment.ActionChooseListener,
|
|
ReplaceFileDialogFragment.ActionChooseListener {
|
|
|
|
private var mDatabase: Database? = null
|
|
|
|
// Refs of an entry and group in database, are not modifiable
|
|
private var mEntry: Entry? = null
|
|
private var mParent: Group? = null
|
|
private var mIsNew: Boolean = false
|
|
|
|
// Views
|
|
private var coordinatorLayout: CoordinatorLayout? = null
|
|
private var scrollView: NestedScrollView? = null
|
|
private var entryEditFragment: EntryEditFragment? = null
|
|
private var entryEditAddToolBar: ToolbarAction? = null
|
|
private var validateButton: View? = null
|
|
private var lockView: View? = null
|
|
|
|
// 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
|
|
|
|
// To ask data lost only one time
|
|
private var backPressedAlreadyApproved = false
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setContentView(R.layout.activity_entry_edit)
|
|
|
|
// Bottom Bar
|
|
entryEditAddToolBar = findViewById(R.id.entry_edit_bottom_bar)
|
|
setSupportActionBar(entryEditAddToolBar)
|
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
supportActionBar?.setDisplayShowHomeEnabled(true)
|
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
|
|
|
coordinatorLayout = findViewById(R.id.entry_edit_coordinator_layout)
|
|
|
|
scrollView = findViewById(R.id.entry_edit_scroll)
|
|
scrollView?.scrollBarStyle = View.SCROLLBARS_INSIDE_INSET
|
|
|
|
lockView = findViewById(R.id.lock_button)
|
|
lockView?.setOnClickListener {
|
|
lockAndExit()
|
|
}
|
|
|
|
// Focus view to reinitialize timeout
|
|
coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this)
|
|
|
|
stopService(Intent(this, ClipboardEntryNotificationService::class.java))
|
|
stopService(Intent(this, KeyboardEntryNotificationService::class.java))
|
|
|
|
// Likely the app has been killed exit the activity
|
|
mDatabase = Database.getInstance()
|
|
|
|
var tempEntryInfo: EntryInfo? = null
|
|
|
|
// Entry is retrieve, it's an entry to update
|
|
intent.getParcelableExtra<NodeId<UUID>>(KEY_ENTRY)?.let {
|
|
mIsNew = false
|
|
// Create an Entry copy to modify from the database entry
|
|
mEntry = mDatabase?.getEntryById(it)
|
|
|
|
// Retrieve the parent
|
|
mEntry?.let { entry ->
|
|
mParent = entry.parent
|
|
// If no parent, add root group as parent
|
|
if (mParent == null) {
|
|
mParent = mDatabase?.rootGroup
|
|
entry.parent = mParent
|
|
}
|
|
}
|
|
tempEntryInfo = mEntry?.getEntryInfo(mDatabase, true)
|
|
}
|
|
|
|
// Parent is retrieve, it's a new entry to create
|
|
intent.getParcelableExtra<NodeId<*>>(KEY_PARENT)?.let {
|
|
mIsNew = true
|
|
mParent = mDatabase?.getGroupById(it)
|
|
// Add the default icon from parent if not a folder
|
|
val parentIcon = mParent?.icon
|
|
tempEntryInfo = mDatabase?.createEntry()?.getEntryInfo(mDatabase, true)
|
|
// Set default icon
|
|
if (parentIcon != null) {
|
|
if (parentIcon.custom.isUnknown
|
|
&& parentIcon.standard.id != IconImageStandard.FOLDER_ID) {
|
|
tempEntryInfo?.icon = IconImage(parentIcon.standard)
|
|
}
|
|
if (!parentIcon.custom.isUnknown) {
|
|
tempEntryInfo?.icon = IconImage(parentIcon.custom)
|
|
}
|
|
}
|
|
// Set default username
|
|
tempEntryInfo?.username = mDatabase?.defaultUsername ?: ""
|
|
}
|
|
|
|
// Retrieve data from registration
|
|
val registerInfo = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
|
registerInfo?.username?.let {
|
|
tempEntryInfo?.username = it
|
|
}
|
|
registerInfo?.password?.let {
|
|
tempEntryInfo?.password = it
|
|
}
|
|
searchInfo?.let { tempSearchInfo ->
|
|
tempEntryInfo?.saveSearchInfo(mDatabase, tempSearchInfo)
|
|
}
|
|
|
|
// Build fragment to manage entry modification
|
|
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
|
|
if (entryEditFragment == null) {
|
|
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo)
|
|
}
|
|
supportFragmentManager.beginTransaction()
|
|
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)
|
|
.commit()
|
|
entryEditFragment?.apply {
|
|
drawFactory = mDatabase?.iconDrawableFactory
|
|
setOnDateClickListener = {
|
|
expiryTime.date.let { expiresDate ->
|
|
val dateTime = DateTime(expiresDate)
|
|
val defaultYear = dateTime.year
|
|
val defaultMonth = dateTime.monthOfYear-1
|
|
val defaultDay = dateTime.dayOfMonth
|
|
DatePickerFragment.getInstance(defaultYear, defaultMonth, defaultDay)
|
|
.show(supportFragmentManager, "DatePickerFragment")
|
|
}
|
|
}
|
|
setOnPasswordGeneratorClickListener = View.OnClickListener {
|
|
openPasswordGenerator()
|
|
}
|
|
// Add listener to the icon
|
|
setOnIconViewClickListener = { iconImage ->
|
|
IconPickerActivity.launch(this@EntryEditActivity, iconImage)
|
|
}
|
|
setOnRemoveAttachment = { attachment ->
|
|
mAttachmentFileBinderManager?.removeBinaryAttachment(attachment)
|
|
removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD))
|
|
}
|
|
setOnEditCustomField = { field ->
|
|
editCustomField(field)
|
|
}
|
|
}
|
|
|
|
// Retrieve temp attachments in case of deletion
|
|
if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) {
|
|
mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments
|
|
}
|
|
|
|
// To retrieve attachment
|
|
mExternalFileHelper = ExternalFileHelper(this)
|
|
mAttachmentFileBinderManager = AttachmentFileBinderManager(this)
|
|
|
|
// Save button
|
|
validateButton = findViewById(R.id.entry_edit_validate)
|
|
validateButton?.setOnClickListener { saveEntry() }
|
|
|
|
// Verify the education views
|
|
entryEditActivityEducation = EntryEditActivityEducation(this)
|
|
|
|
// Create progress dialog
|
|
mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result ->
|
|
when (actionTask) {
|
|
ACTION_DATABASE_CREATE_ENTRY_TASK,
|
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
|
try {
|
|
if (result.isSuccess) {
|
|
var newNodes: List<Node> = ArrayList()
|
|
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
|
mDatabase?.let { database ->
|
|
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
|
}
|
|
}
|
|
if (newNodes.size == 1) {
|
|
(newNodes[0] as? Entry?)?.let { entry ->
|
|
mEntry = entry
|
|
EntrySelectionHelper.doSpecialAction(intent,
|
|
{
|
|
// Finish naturally
|
|
finishForEntryResult()
|
|
},
|
|
{
|
|
// Nothing when search retrieved
|
|
},
|
|
{
|
|
entryValidatedForSave()
|
|
},
|
|
{
|
|
entryValidatedForKeyboardSelection(entry)
|
|
},
|
|
{ _, _ ->
|
|
entryValidatedForAutofillSelection(entry)
|
|
},
|
|
{
|
|
entryValidatedForAutofillRegistration()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "Unable to retrieve entry after database action", e)
|
|
}
|
|
}
|
|
ACTION_DATABASE_RELOAD_TASK -> {
|
|
// Close the current activity
|
|
this.showActionErrorIfNeeded(result)
|
|
finish()
|
|
}
|
|
}
|
|
coordinatorLayout?.showActionErrorIfNeeded(result)
|
|
}
|
|
}
|
|
|
|
private fun entryValidatedForSave() {
|
|
onValidateSpecialMode()
|
|
finishForEntryResult()
|
|
}
|
|
|
|
private fun entryValidatedForKeyboardSelection(entry: Entry) {
|
|
// Populate Magikeyboard with entry
|
|
mDatabase?.let { database ->
|
|
populateKeyboardAndMoveAppToBackground(this,
|
|
entry.getEntryInfo(database),
|
|
intent)
|
|
}
|
|
onValidateSpecialMode()
|
|
// Don't keep activity history for entry edition
|
|
finishForEntryResult()
|
|
}
|
|
|
|
private fun entryValidatedForAutofillSelection(entry: Entry) {
|
|
// Build Autofill response with the entry selected
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
mDatabase?.let { database ->
|
|
AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity,
|
|
entry.getEntryInfo(database))
|
|
}
|
|
}
|
|
onValidateSpecialMode()
|
|
}
|
|
|
|
private fun entryValidatedForAutofillRegistration() {
|
|
onValidateSpecialMode()
|
|
finishForEntryResult()
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
|
|
lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) {
|
|
View.VISIBLE
|
|
} else {
|
|
View.GONE
|
|
}
|
|
|
|
// Padding if lock button visible
|
|
entryEditAddToolBar?.updateLockPaddingLeft()
|
|
|
|
mAllowMultipleAttachments = mDatabase?.allowMultipleAttachments == true
|
|
mAttachmentFileBinderManager?.apply {
|
|
registerProgressTask()
|
|
onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener {
|
|
override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) {
|
|
when (entryAttachmentState.downloadState) {
|
|
AttachmentState.START -> {
|
|
entryEditFragment?.apply {
|
|
putAttachment(entryAttachmentState)
|
|
// Scroll to the attachment position
|
|
getAttachmentViewPosition(entryAttachmentState) {
|
|
scrollView?.smoothScrollTo(0, it.toInt())
|
|
}
|
|
} // Add in temp list
|
|
mTempAttachments.add(entryAttachmentState)
|
|
}
|
|
AttachmentState.IN_PROGRESS -> {
|
|
entryEditFragment?.putAttachment(entryAttachmentState)
|
|
}
|
|
AttachmentState.COMPLETE -> {
|
|
entryEditFragment?.putAttachment(entryAttachmentState) {
|
|
entryEditFragment?.getAttachmentViewPosition(entryAttachmentState) {
|
|
scrollView?.smoothScrollTo(0, it.toInt())
|
|
}
|
|
}
|
|
}
|
|
AttachmentState.CANCELED -> {
|
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
|
}
|
|
AttachmentState.ERROR -> {
|
|
entryEditFragment?.removeAttachment(entryAttachmentState)
|
|
coordinatorLayout?.let {
|
|
Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show()
|
|
}
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
mAttachmentFileBinderManager?.unregisterProgressTask()
|
|
|
|
super.onPause()
|
|
}
|
|
|
|
/**
|
|
* Open the password generator fragment
|
|
*/
|
|
private fun openPasswordGenerator() {
|
|
GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment")
|
|
}
|
|
|
|
/**
|
|
* Add a new customized field
|
|
*/
|
|
private fun addNewCustomField() {
|
|
EntryCustomFieldDialogFragment.getInstance().show(supportFragmentManager, "customFieldDialog")
|
|
}
|
|
|
|
private fun editCustomField(field: Field) {
|
|
EntryCustomFieldDialogFragment.getInstance(field).show(supportFragmentManager, "customFieldDialog")
|
|
}
|
|
|
|
private fun verifyNameField(field: Field,
|
|
actionIfNewName: () -> Unit) {
|
|
var extraFieldAlreadyContainsName = false
|
|
entryEditFragment?.getExtraFields()?.forEach {
|
|
if (it.name.equals(field.name, true))
|
|
extraFieldAlreadyContainsName = true
|
|
}
|
|
|
|
if (!extraFieldAlreadyContainsName
|
|
&& Entry.newExtraFieldNameAllowed(field)) {
|
|
actionIfNewName.invoke()
|
|
} else {
|
|
Log.e(TAG, "Unable to create the new field, field name already exists")
|
|
coordinatorLayout?.let {
|
|
Snackbar.make(it, R.string.error_field_name_already_exists, Snackbar.LENGTH_LONG).asError().show()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onNewCustomFieldApproved(newField: Field) {
|
|
verifyNameField(newField) {
|
|
entryEditFragment?.putExtraField(newField)
|
|
}
|
|
}
|
|
|
|
override fun onEditCustomFieldApproved(oldField: Field, newField: Field) {
|
|
if (oldField.name.equals(newField.name, true)) {
|
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
|
} else {
|
|
verifyNameField(newField) {
|
|
entryEditFragment?.replaceExtraField(oldField, newField)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onDeleteCustomFieldApproved(oldField: Field) {
|
|
entryEditFragment?.removeExtraField(oldField)
|
|
}
|
|
|
|
/**
|
|
* Add a new attachment
|
|
*/
|
|
private fun addNewAttachment() {
|
|
mExternalFileHelper?.openDocument()
|
|
}
|
|
|
|
override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) {
|
|
if (attachmentToUploadUri != null && fileName != null) {
|
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
|
}
|
|
}
|
|
|
|
override fun onValidateReplaceFile(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
|
startUploadAttachment(attachmentToUploadUri, attachment)
|
|
}
|
|
|
|
private fun startUploadAttachment(attachmentToUploadUri: Uri?, attachment: Attachment?) {
|
|
if (attachmentToUploadUri != null && attachment != null) {
|
|
// When only one attachment is allowed
|
|
if (!mAllowMultipleAttachments) {
|
|
entryEditFragment?.clearAttachments()
|
|
}
|
|
// Start uploading in service
|
|
mAttachmentFileBinderManager?.startUploadAttachment(attachmentToUploadUri, attachment)
|
|
}
|
|
}
|
|
|
|
private fun buildNewAttachment(attachmentToUploadUri: Uri, fileName: String) {
|
|
val compression = mDatabase?.compressionForNewEntry() ?: false
|
|
mDatabase?.buildNewBinaryAttachment(compression)?.let { binaryAttachment ->
|
|
val entryAttachment = Attachment(fileName, binaryAttachment)
|
|
// Ask to replace the current attachment
|
|
if ((mDatabase?.allowMultipleAttachments != true && entryEditFragment?.containsAttachment() == true) ||
|
|
entryEditFragment?.containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD)) == true) {
|
|
ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment)
|
|
.show(supportFragmentManager, "replacementFileFragment")
|
|
} else {
|
|
startUploadAttachment(attachmentToUploadUri, entryAttachment)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
super.onActivityResult(requestCode, resultCode, data)
|
|
|
|
IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon ->
|
|
entryEditFragment?.icon = icon
|
|
}
|
|
|
|
mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri ->
|
|
uri?.let { attachmentToUploadUri ->
|
|
UriUtil.getFileData(this, attachmentToUploadUri)?.also { documentFile ->
|
|
documentFile.name?.let { fileName ->
|
|
if (documentFile.length() > MAX_WARNING_BINARY_FILE) {
|
|
FileTooBigDialogFragment.build(attachmentToUploadUri, fileName)
|
|
.show(supportFragmentManager, "fileTooBigFragment")
|
|
} else {
|
|
buildNewAttachment(attachmentToUploadUri, fileName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(entryEditFragment?.getEntryInfo()?.otpModel)
|
|
.show(supportFragmentManager, "addOTPDialog")
|
|
}
|
|
|
|
/**
|
|
* Saves the new entry or update an existing entry in the database
|
|
*/
|
|
private fun saveEntry() {
|
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
|
// Get the temp entry
|
|
entryEditFragment?.getEntryInfo()?.let { newEntryInfo ->
|
|
|
|
if (mIsNew) {
|
|
// Create new one
|
|
mDatabase?.createEntry()
|
|
} else {
|
|
// Create a clone
|
|
Entry(mEntry!!)
|
|
}?.let { newEntry ->
|
|
|
|
// 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 -> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build info
|
|
newEntry.setEntryInfo(mDatabase, newEntryInfo)
|
|
|
|
// 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
|
|
if (mIsNew) {
|
|
mParent?.let { parent ->
|
|
mProgressDatabaseTaskProvider?.startDatabaseCreateEntry(
|
|
newEntry,
|
|
parent,
|
|
!mReadOnly && mAutoSaveEnable
|
|
)
|
|
}
|
|
} else {
|
|
mEntry?.let { oldEntry ->
|
|
mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry(
|
|
oldEntry,
|
|
newEntry,
|
|
!mReadOnly && mAutoSaveEnable
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
super.onCreateOptionsMenu(menu)
|
|
menuInflater.inflate(R.menu.entry_edit, menu)
|
|
return true
|
|
}
|
|
|
|
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
|
|
|
menu?.findItem(R.id.menu_add_field)?.apply {
|
|
val allowCustomField = mDatabase?.allowEntryCustomFields() == true
|
|
isEnabled = allowCustomField
|
|
isVisible = allowCustomField
|
|
}
|
|
|
|
// Attachment not compatible below KitKat
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
|
menu?.findItem(R.id.menu_add_attachment)?.isVisible = false
|
|
}
|
|
|
|
menu?.findItem(R.id.menu_add_otp)?.apply {
|
|
val allowOTP = mDatabase?.allowOTP == true
|
|
isEnabled = allowOTP
|
|
// OTP not compatible below KitKat
|
|
isVisible = allowOTP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
}
|
|
|
|
entryEditActivityEducation?.let {
|
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
|
}
|
|
return super.onPrepareOptionsMenu(menu)
|
|
}
|
|
|
|
fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) {
|
|
if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) {
|
|
val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field)
|
|
val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true
|
|
&& addNewFieldView != null
|
|
&& addNewFieldView.isVisible
|
|
&& entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation(
|
|
addNewFieldView,
|
|
{
|
|
addNewCustomField()
|
|
},
|
|
{
|
|
performedNextEducation(entryEditActivityEducation)
|
|
}
|
|
)
|
|
if (!addNewFieldEducationPerformed) {
|
|
val attachmentView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_attachment)
|
|
val addAttachmentEducationPerformed = attachmentView != null
|
|
&& attachmentView.isVisible
|
|
&& entryEditActivityEducation.checkAndPerformedAttachmentEducation(
|
|
attachmentView,
|
|
{
|
|
mExternalFileHelper?.openDocument()
|
|
},
|
|
{
|
|
performedNextEducation(entryEditActivityEducation)
|
|
}
|
|
)
|
|
if (!addAttachmentEducationPerformed) {
|
|
val setupOtpView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_otp)
|
|
setupOtpView != null
|
|
&& setupOtpView.isVisible
|
|
&& entryEditActivityEducation.checkAndPerformedSetUpOTPEducation(
|
|
setupOtpView,
|
|
{
|
|
setupOTP()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
when (item.itemId) {
|
|
R.id.menu_add_field -> {
|
|
addNewCustomField()
|
|
return true
|
|
}
|
|
R.id.menu_add_attachment -> {
|
|
addNewAttachment()
|
|
return true
|
|
}
|
|
R.id.menu_add_otp -> {
|
|
setupOTP()
|
|
return true
|
|
}
|
|
android.R.id.home -> {
|
|
onBackPressed()
|
|
}
|
|
}
|
|
|
|
return super.onOptionsItemSelected(item)
|
|
}
|
|
|
|
override fun onOtpCreated(otpElement: OtpElement) {
|
|
var titleOTP: String? = null
|
|
var usernameOTP: String? = null
|
|
// Build a temp entry to get title and username (by ref)
|
|
entryEditFragment?.getEntryInfo()?.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 {
|
|
putExtraField(otpField)
|
|
}
|
|
}
|
|
|
|
override fun onDateSet(datePicker: DatePicker?, year: Int, month: Int, day: Int) {
|
|
// To fix android 4.4 issue
|
|
// https://stackoverflow.com/questions/12436073/datepicker-ondatechangedlistener-called-twice
|
|
if (datePicker?.isShown == true) {
|
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
|
// Save the date
|
|
entryEditFragment?.expiryTime =
|
|
DateInstant(DateTime(expiresDate)
|
|
.withYear(year)
|
|
.withMonthOfYear(month + 1)
|
|
.withDayOfMonth(day)
|
|
.toDate())
|
|
// Launch the time picker
|
|
val dateTime = DateTime(expiresDate)
|
|
val defaultHour = dateTime.hourOfDay
|
|
val defaultMinute = dateTime.minuteOfHour
|
|
TimePickerFragment.getInstance(defaultHour, defaultMinute)
|
|
.show(supportFragmentManager, "TimePickerFragment")
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onTimeSet(timePicker: TimePicker?, hours: Int, minutes: Int) {
|
|
entryEditFragment?.expiryTime?.date?.let { expiresDate ->
|
|
// Save the date
|
|
entryEditFragment?.expiryTime =
|
|
DateInstant(DateTime(expiresDate)
|
|
.withHourOfDay(hours)
|
|
.withMinuteOfHour(minutes)
|
|
.toDate())
|
|
}
|
|
}
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
outState.putParcelableArrayList(TEMP_ATTACHMENTS, mTempAttachments)
|
|
|
|
super.onSaveInstanceState(outState)
|
|
}
|
|
|
|
override fun acceptPassword(bundle: Bundle) {
|
|
bundle.getString(GeneratePasswordDialogFragment.KEY_PASSWORD_ID)?.let {
|
|
entryEditFragment?.password = it
|
|
}
|
|
|
|
entryEditActivityEducation?.let {
|
|
Handler(Looper.getMainLooper()).post { performedNextEducation(it) }
|
|
}
|
|
}
|
|
|
|
override fun cancelPassword(bundle: Bundle) {
|
|
// Do nothing here
|
|
}
|
|
|
|
override fun onBackPressed() {
|
|
onApprovedBackPressed {
|
|
super@EntryEditActivity.onBackPressed()
|
|
}
|
|
}
|
|
|
|
override fun onCancelSpecialMode() {
|
|
onApprovedBackPressed {
|
|
super@EntryEditActivity.onCancelSpecialMode()
|
|
finish()
|
|
}
|
|
}
|
|
|
|
private fun onApprovedBackPressed(approved: () -> Unit) {
|
|
if (!backPressedAlreadyApproved) {
|
|
AlertDialog.Builder(this)
|
|
.setMessage(R.string.discard_changes)
|
|
.setNegativeButton(android.R.string.cancel, null)
|
|
.setPositiveButton(R.string.discard) { _, _ ->
|
|
mAttachmentFileBinderManager?.stopUploadAllAttachments()
|
|
backPressedAlreadyApproved = true
|
|
approved.invoke()
|
|
}.create().show()
|
|
} else {
|
|
approved.invoke()
|
|
}
|
|
}
|
|
|
|
private fun finishForEntryResult() {
|
|
// 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 (mIsNew) {
|
|
setResult(ADD_ENTRY_RESULT_CODE, intentEntry)
|
|
} else {
|
|
setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry)
|
|
}
|
|
}
|
|
super.finish()
|
|
} catch (e: Exception) {
|
|
// Exception when parcelable can't be done
|
|
Log.e(TAG, "Cant add entry as result", e)
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
|
|
private val TAG = EntryEditActivity::class.java.name
|
|
|
|
// Keys for current Activity
|
|
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
|
|
const val ADD_OR_UPDATE_ENTRY_REQUEST_CODE = 7129
|
|
const val ADD_OR_UPDATE_ENTRY_KEY = "ADD_OR_UPDATE_ENTRY_KEY"
|
|
|
|
const val ENTRY_EDIT_FRAGMENT_TAG = "ENTRY_EDIT_FRAGMENT_TAG"
|
|
|
|
/**
|
|
* Launch EntryEditActivity to update an existing entry
|
|
*
|
|
* @param activity from activity
|
|
* @param entry Entry to update
|
|
*/
|
|
fun launch(activity: Activity,
|
|
entry: Entry) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
|
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launch EntryEditActivity to add a new entry
|
|
*
|
|
* @param activity from activity
|
|
* @param group Group who will contains new entry
|
|
*/
|
|
fun launch(activity: Activity,
|
|
group: Group) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
|
activity.startActivityForResult(intent, ADD_OR_UPDATE_ENTRY_REQUEST_CODE)
|
|
}
|
|
}
|
|
|
|
fun launchForSave(context: Context,
|
|
entry: Entry,
|
|
searchInfo: SearchInfo) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
val intent = Intent(context, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
|
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
|
intent,
|
|
searchInfo)
|
|
}
|
|
}
|
|
|
|
fun launchForSave(context: Context,
|
|
group: Group,
|
|
searchInfo: SearchInfo) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
val intent = Intent(context, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
|
EntrySelectionHelper.startActivityForSaveModeResult(context,
|
|
intent,
|
|
searchInfo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launch EntryEditActivity to add a new entry in keyboard selection
|
|
*/
|
|
fun launchForKeyboardSelectionResult(context: Context,
|
|
group: Group,
|
|
searchInfo: SearchInfo? = null) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
val intent = Intent(context, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
|
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context,
|
|
intent,
|
|
searchInfo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launch EntryEditActivity to add a new entry in autofill selection
|
|
*/
|
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
fun launchForAutofillResult(activity: Activity,
|
|
autofillComponent: AutofillComponent,
|
|
group: Group,
|
|
searchInfo: SearchInfo? = null) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
|
AutofillHelper.startActivityForAutofillResult(activity,
|
|
intent,
|
|
autofillComponent,
|
|
searchInfo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launch EntryEditActivity to register an updated entry (from autofill)
|
|
*/
|
|
fun launchForRegistration(context: Context,
|
|
entry: Entry,
|
|
registerInfo: RegisterInfo? = null) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
val intent = Intent(context, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_ENTRY, entry.nodeId)
|
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
|
intent,
|
|
registerInfo)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Launch EntryEditActivity to register a new entry (from autofill)
|
|
*/
|
|
fun launchForRegistration(context: Context,
|
|
group: Group,
|
|
registerInfo: RegisterInfo? = null) {
|
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
|
val intent = Intent(context, EntryEditActivity::class.java)
|
|
intent.putExtra(KEY_PARENT, group.nodeId)
|
|
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
|
intent,
|
|
registerInfo)
|
|
}
|
|
}
|
|
}
|
|
}
|