diff --git a/CHANGELOG b/CHANGELOG index d9b5b1ba1..44c0ccc51 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +KeePassDX(3.0.0) + * Add / Manage dynamic templates #191 + * Manually select RecycleBin group and Templates group #191 + * Setting to display OTP Token in list #655 + * Fix timeout in dialogs #716 + * Check URI permissions #626 + * Better autofill implementation #943 #946 #984 #1070 (Thx @uduerholz) + * Improvements #680 #1035 #1043 #942 #1021 #1027 #1046 #1082 #1083 (Thx @chenxiaolong) + KeePassDX(2.10.5) * Increase the saving speed of database #1028 * Fix advanced unlocking by device credential #1029 diff --git a/app/build.gradle b/app/build.gradle index 62933eb17..29c3b1e3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.kunzisoft.keepass" minSdkVersion 15 targetSdkVersion 30 - versionCode = 83 - versionName = "2.10.5" + versionCode = 87 + versionName = "3.0.0" multiDexEnabled true testApplicationId = "com.kunzisoft.keepass.tests" diff --git a/app/src/androidTest/java/com/kunzisoft/keepass/tests/template/TemplateAttributeOptionTest.kt b/app/src/androidTest/java/com/kunzisoft/keepass/tests/template/TemplateAttributeOptionTest.kt new file mode 100644 index 000000000..66fe35079 --- /dev/null +++ b/app/src/androidTest/java/com/kunzisoft/keepass/tests/template/TemplateAttributeOptionTest.kt @@ -0,0 +1,24 @@ +package com.kunzisoft.keepass.tests.template + +import com.kunzisoft.keepass.database.element.template.TemplateAttributeOption +import junit.framework.TestCase +import org.junit.Assert + +class TemplateAttributeOptionTest: TestCase() { + + fun testSerializeOptions() { + val options = TemplateAttributeOption().apply { + put("TestA", "TestB") + put("{D", "}C") + put("E,gyu", "15,jk") + put("ù*:**", "78:96?545") + } + + val strings = TemplateAttributeOption.getStringFromOptions(options) + val optionsAfterSerialization = TemplateAttributeOption.getOptionsFromString(strings) + val otherString = TemplateAttributeOption.getStringFromOptions(optionsAfterSerialization) + + Assert.assertEquals("Output not equal to input", strings, otherString) + } + +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f4d196b2..72898c755 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,7 +45,7 @@ android:label="@string/app_name" android:launchMode="singleTop" android:configChanges="keyboardHidden" - android:windowSoftInputMode="stateHidden" > + android:windowSoftInputMode="stateHidden|stateAlwaysHidden" > @@ -112,8 +112,7 @@ + android:windowSoftInputMode="adjustPan"> = WeakReference(imageView) private var containerRef: WeakReference = WeakReference(container) + @SuppressLint("ClickableViewAccessibility") override fun onTouch(view: View?, event: MotionEvent?): Boolean { + onViewTouchedListener?.onTouch(view, event) + event ?: return false val imageView = imageViewRef.get() ?: return false val container = containerRef.get() ?: return false diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt index dd1433aed..43a73b1fb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt @@ -25,14 +25,13 @@ import android.content.Context import android.content.Intent import android.content.IntentSender import android.os.Build -import android.os.Bundle import android.view.inputmethod.InlineSuggestionsRequest import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper.EXTRA_INLINE_SUGGESTIONS_REQUEST import com.kunzisoft.keepass.autofill.KeeAutofillService @@ -44,9 +43,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.utils.LOCK_ACTION @RequiresApi(api = Build.VERSION_CODES.O) -class AutofillLauncherActivity : AppCompatActivity() { +class AutofillLauncherActivity : DatabaseModeActivity() { - override fun onCreate(savedInstanceState: Bundle?) { + override fun applyCustomStyle(): Boolean { + return false + } + + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) // Retrieve selection mode EntrySelectionHelper.retrieveSpecialModeFromIntent(intent).let { specialMode -> @@ -57,10 +65,11 @@ class AutofillLauncherActivity : AppCompatActivity() { applicationId = intent.getStringExtra(KEY_SEARCH_APPLICATION_ID) webDomain = intent.getStringExtra(KEY_SEARCH_DOMAIN) webScheme = intent.getStringExtra(KEY_SEARCH_SCHEME) + manualSelection = intent.getBooleanExtra(KEY_MANUAL_SELECTION, false) } SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> searchInfo.webDomain = concreteWebDomain - launchSelection(searchInfo) + launchSelection(database, searchInfo) } } SpecialMode.REGISTRATION -> { @@ -69,7 +78,7 @@ class AutofillLauncherActivity : AppCompatActivity() { val searchInfo = SearchInfo(registerInfo?.searchInfo) SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> searchInfo.webDomain = concreteWebDomain - launchRegistration(searchInfo, registerInfo) + launchRegistration(database, searchInfo, registerInfo) } } else -> { @@ -79,11 +88,10 @@ class AutofillLauncherActivity : AppCompatActivity() { } } } - - super.onCreate(savedInstanceState) } - private fun launchSelection(searchInfo: SearchInfo) { + private fun launchSelection(database: Database?, + searchInfo: SearchInfo) { // Pass extra for Autofill (EXTRA_ASSIST_STRUCTURE) val autofillComponent = AutofillHelper.retrieveAutofillComponent(intent) @@ -98,24 +106,22 @@ class AutofillLauncherActivity : AppCompatActivity() { setResult(Activity.RESULT_CANCELED) finish() } else { - val database = Database.getInstance() - val readOnly = database.isReadOnly // If database is open SearchHelper.checkAutoSearchInfo(this, - Database.getInstance(), + database, searchInfo, - { items -> + { openedDatabase, items -> // Items found - AutofillHelper.buildResponseAndSetResult(this, items) + AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items) finish() }, - { + { openedDatabase -> // Show the database UI to select the entry GroupActivity.launchForAutofillResult(this, - readOnly, - autofillComponent, - searchInfo, - false) + openedDatabase, + autofillComponent, + searchInfo, + false) }, { // If database not open @@ -127,7 +133,9 @@ class AutofillLauncherActivity : AppCompatActivity() { } } - private fun launchRegistration(searchInfo: SearchInfo, registerInfo: RegisterInfo?) { + private fun launchRegistration(database: Database?, + searchInfo: SearchInfo, + registerInfo: RegisterInfo?) { if (!KeeAutofillService.autofillAllowedFor(searchInfo.applicationId, PreferencesUtil.applicationIdBlocklist(this)) || !KeeAutofillService.autofillAllowedFor(searchInfo.webDomain, @@ -135,25 +143,26 @@ class AutofillLauncherActivity : AppCompatActivity() { showBlockRestartMessage() setResult(Activity.RESULT_CANCELED) } else { - val database = Database.getInstance() - val readOnly = database.isReadOnly + val readOnly = database?.isReadOnly != false SearchHelper.checkAutoSearchInfo(this, database, searchInfo, - { _ -> + { openedDatabase, _ -> if (!readOnly) { // Show the database UI to select the entry GroupActivity.launchForRegistration(this, - registerInfo) + openedDatabase, + registerInfo) } else { showReadOnlySaveMessage() } }, - { + { openedDatabase -> if (!readOnly) { // Show the database UI to select the entry GroupActivity.launchForRegistration(this, - registerInfo) + openedDatabase, + registerInfo) } else { showReadOnlySaveMessage() } @@ -190,15 +199,16 @@ class AutofillLauncherActivity : AppCompatActivity() { companion object { + private const val KEY_MANUAL_SELECTION = "KEY_MANUAL_SELECTION" private const val KEY_SEARCH_APPLICATION_ID = "KEY_SEARCH_APPLICATION_ID" private const val KEY_SEARCH_DOMAIN = "KEY_SEARCH_DOMAIN" private const val KEY_SEARCH_SCHEME = "KEY_SEARCH_SCHEME" private const val KEY_REGISTER_INFO = "KEY_REGISTER_INFO" - fun getAuthIntentSenderForSelection(context: Context, - searchInfo: SearchInfo? = null, - inlineSuggestionsRequest: InlineSuggestionsRequest? = null): IntentSender { + fun getPendingIntentForSelection(context: Context, + searchInfo: SearchInfo? = null, + inlineSuggestionsRequest: InlineSuggestionsRequest? = null): PendingIntent { return PendingIntent.getActivity(context, 0, // Doesn't work with Parcelable (don't know why?) Intent(context, AutofillLauncherActivity::class.java).apply { @@ -206,6 +216,7 @@ class AutofillLauncherActivity : AppCompatActivity() { putExtra(KEY_SEARCH_APPLICATION_ID, it.applicationId) putExtra(KEY_SEARCH_DOMAIN, it.webDomain) putExtra(KEY_SEARCH_SCHEME, it.webScheme) + putExtra(KEY_MANUAL_SELECTION, it.manualSelection) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { inlineSuggestionsRequest?.let { @@ -213,17 +224,17 @@ class AutofillLauncherActivity : AppCompatActivity() { } } }, - PendingIntent.FLAG_CANCEL_CURRENT).intentSender + PendingIntent.FLAG_CANCEL_CURRENT) } - fun getAuthIntentSenderForRegistration(context: Context, - registerInfo: RegisterInfo): IntentSender { + fun getPendingIntentForRegistration(context: Context, + registerInfo: RegisterInfo): PendingIntent { return PendingIntent.getActivity(context, 0, Intent(context, AutofillLauncherActivity::class.java).apply { EntrySelectionHelper.addSpecialModeInIntent(this, SpecialMode.REGISTRATION) putExtra(KEY_REGISTER_INFO, registerInfo) }, - PendingIntent.FLAG_CANCEL_CURRENT).intentSender + PendingIntent.FLAG_CANCEL_CURRENT) } fun launchForRegistration(context: Context, diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index a6090f132..eb03b76da 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -32,71 +32,63 @@ import android.view.MenuItem import android.view.View import android.widget.ImageView import android.widget.ProgressBar -import android.widget.Toast -import androidx.appcompat.app.AlertDialog +import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.appbar.CollapsingToolbarLayout import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper +import com.kunzisoft.keepass.activities.fragments.EntryFragment import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.education.EntryActivityEducation -import com.kunzisoft.keepass.magikeyboard.MagikIME +import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.model.EntryAttachmentState -import com.kunzisoft.keepass.model.StreamDirection -import com.kunzisoft.keepass.otp.OtpEntryFields +import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.services.AttachmentFileNotificationService import com.kunzisoft.keepass.services.ClipboardEntryNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_ENTRY_HISTORY -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RELOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_RESTORE_ENTRY_HISTORY import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager -import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.* -import com.kunzisoft.keepass.view.EntryContentsView +import com.kunzisoft.keepass.view.hideByFading import com.kunzisoft.keepass.view.showActionErrorIfNeeded +import com.kunzisoft.keepass.viewmodels.EntryViewModel import java.util.* import kotlin.collections.HashMap -class EntryActivity : LockingActivity() { +class EntryActivity : DatabaseLockActivity() { private var coordinatorLayout: CoordinatorLayout? = null private var collapsingToolbarLayout: CollapsingToolbarLayout? = null private var titleIconView: ImageView? = null private var historyView: View? = null - private var entryContentsView: EntryContentsView? = null private var entryProgress: ProgressBar? = null private var lockView: View? = null private var toolbar: Toolbar? = null + private var loadingView: ProgressBar? = null - private var mDatabase: Database? = null + private val mEntryViewModel: EntryViewModel by viewModels() - private var mEntry: Entry? = null - - private var mIsHistory: Boolean = false - private var mEntryLastVersion: Entry? = null - private var mEntryHistoryPosition: Int = -1 - - private var mShowPassword: Boolean = false + private var mMainEntryId: NodeId? = null + private var mHistoryPosition: Int = -1 + private var mEntryIsHistory: Boolean = false + private var mUrl: String? = null + private var mEntryLoaded = false private var mAttachmentFileBinderManager: AttachmentFileBinderManager? = null private var mAttachmentsToDownload: HashMap = HashMap() - - private var clipboardHelper: ClipboardHelper? = null - private var mFirstLaunchOfActivity: Boolean = false - private var mExternalFileHelper: ExternalFileHelper? = null - private var iconColor: Int = 0 + private var mIcon: IconImage? = null + private var mIconColor: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -108,282 +100,204 @@ class EntryActivity : LockingActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) - mDatabase = Database.getInstance() - mReadOnly = mDatabase!!.isReadOnly || mReadOnly - - mShowPassword = !PreferencesUtil.isPasswordMask(this) - - // Retrieve the textColor to tint the icon - val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) - iconColor = taIconColor.getColor(0, Color.BLACK) - taIconColor.recycle() - - // Refresh Menu contents in case onCreateMenuOptions was called before mEntry was set - invalidateOptionsMenu() - // Get views coordinatorLayout = findViewById(R.id.toolbar_coordinator) collapsingToolbarLayout = findViewById(R.id.toolbar_layout) titleIconView = findViewById(R.id.entry_icon) historyView = findViewById(R.id.history_container) - entryContentsView = findViewById(R.id.entry_contents) - entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) - entryContentsView?.setAttachmentCipherKey(mDatabase) entryProgress = findViewById(R.id.entry_progress) lockView = findViewById(R.id.lock_button) + loadingView = findViewById(R.id.loading) - lockView?.setOnClickListener { - lockAndExit() - } + // Empty title + collapsingToolbarLayout?.title = " " + toolbar?.title = " " - // Focus view to reinitialize timeout - coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) - - // Init the clipboard helper - clipboardHelper = ClipboardHelper(this) - mFirstLaunchOfActivity = savedInstanceState?.getBoolean(KEY_FIRST_LAUNCH_ACTIVITY) ?: true - - // Init SAF manager - mExternalFileHelper = ExternalFileHelper(this) - - // Init attachment service binder manager - mAttachmentFileBinderManager = AttachmentFileBinderManager(this) - - mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> - when (actionTask) { - ACTION_DATABASE_RESTORE_ENTRY_HISTORY, - ACTION_DATABASE_DELETE_ENTRY_HISTORY -> { - // Close the current activity after an history action - if (result.isSuccess) - finish() - } - ACTION_DATABASE_RELOAD_TASK -> { - // Close the current activity - this.showActionErrorIfNeeded(result) - finish() - } - } - coordinatorLayout?.showActionErrorIfNeeded(result) - } - } - - override fun onResume() { - super.onResume() - - // Show the lock button - lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { - View.VISIBLE - } else { - View.GONE - } + // Retrieve the textColor to tint the icon + val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + mIconColor = taIconColor.getColor(0, Color.BLACK) + taIconColor.recycle() // Get Entry from UUID try { - val keyEntry: NodeId? = intent.getParcelableExtra(KEY_ENTRY) - if (keyEntry != null) { - mEntry = mDatabase?.getEntryById(keyEntry) - mEntryLastVersion = mEntry + intent.getParcelableExtra?>(KEY_ENTRY)?.let { mainEntryId -> + intent.removeExtra(KEY_ENTRY) + val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, -1) + intent.removeExtra(KEY_ENTRY_HISTORY_POSITION) + + mEntryViewModel.loadEntry(mDatabase, mainEntryId, historyPosition) } } catch (e: ClassCastException) { Log.e(TAG, "Unable to retrieve the entry key") } - val historyPosition = intent.getIntExtra(KEY_ENTRY_HISTORY_POSITION, mEntryHistoryPosition) - mEntryHistoryPosition = historyPosition - if (historyPosition >= 0) { - mIsHistory = true - mEntry = mEntry?.getHistory()?.get(historyPosition) - } + // Init SAF manager + mExternalFileHelper = ExternalFileHelper(this) + // Init attachment service binder manager + mAttachmentFileBinderManager = AttachmentFileBinderManager(this) - if (mEntry == null) { - Toast.makeText(this, R.string.entry_not_found, Toast.LENGTH_LONG).show() - finish() - return + lockView?.setOnClickListener { + lockAndExit() } - // Update last access time. - mEntry?.touch(modified = false, touchParents = false) + mEntryViewModel.entryInfoHistory.observe(this) { entryInfoHistory -> + if (entryInfoHistory != null) { + this.mMainEntryId = entryInfoHistory.mainEntryId + + // Manage history position + val historyPosition = entryInfoHistory.historyPosition + this.mHistoryPosition = historyPosition + val entryIsHistory = historyPosition > -1 + this.mEntryIsHistory = entryIsHistory + // Assign history dedicated view + historyView?.visibility = if (entryIsHistory) View.VISIBLE else View.GONE + if (entryIsHistory) { + val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) + collapsingToolbarLayout?.contentScrim = + ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) + taColorAccent.recycle() + } - mEntry?.let { entry -> - // Fill data in resume to update from EntryEditActivity - fillEntryDataInContentsView(entry) + val entryInfo = entryInfoHistory.entryInfo + // Manage entry copy to start notification if allowed (at the first start) + if (savedInstanceState == null) { + // Manage entry to launch copying notification if allowed + ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo) + // Manage entry to populate Magikeyboard and launch keyboard notification if allowed + if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) { + MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(this, entryInfo) + } + } + // Assign title icon + mIcon = entryInfo.icon + titleIconView?.let { iconView -> + mIconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, mIconColor) + } + // Assign title text + val entryTitle = + if (entryInfo.title.isNotEmpty()) entryInfo.title else entryInfo.id.toString() + collapsingToolbarLayout?.title = entryTitle + toolbar?.title = entryTitle + mUrl = entryInfo.url + + loadingView?.hideByFading() + mEntryLoaded = true + } else { + finish() + } // Refresh Menu invalidateOptionsMenu() + } - val entryInfo = entry.getEntryInfo(mDatabase) - // Manage entry copy to start notification if allowed - if (mFirstLaunchOfActivity) { - // Manage entry to launch copying notification if allowed - ClipboardEntryNotificationService.launchNotificationIfAllowed(this, entryInfo) - // Manage entry to populate Magikeyboard and launch keyboard notification if allowed - if (PreferencesUtil.isKeyboardEntrySelectionEnable(this)) { - MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo) + mEntryViewModel.onOtpElementUpdated.observe(this) { otpElement -> + if (otpElement == null) + entryProgress?.visibility = View.GONE + when (otpElement?.type) { + // Only add token if HOTP + OtpType.HOTP -> { + entryProgress?.visibility = View.GONE + } + // Refresh view if TOTP + OtpType.TOTP -> { + entryProgress?.apply { + max = otpElement.period + progress = otpElement.secondsRemaining + visibility = View.VISIBLE + } } } } - mAttachmentFileBinderManager?.apply { - registerProgressTask() - onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { - override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { - if (entryAttachmentState.streamDirection != StreamDirection.UPLOAD) { - entryContentsView?.putAttachment(entryAttachmentState) - } - } + mEntryViewModel.attachmentSelected.observe(this) { attachmentSelected -> + mExternalFileHelper?.createDocument(attachmentSelected.name)?.let { requestCode -> + mAttachmentsToDownload[requestCode] = attachmentSelected } } - mFirstLaunchOfActivity = false + mEntryViewModel.historySelected.observe(this) { historySelected -> + mDatabase?.let { database -> + launch( + this, + database, + historySelected.nodeId, + historySelected.historyPosition + ) + } + } } - override fun onPause() { - mAttachmentFileBinderManager?.unregisterProgressTask() + override fun finishActivityIfReloadRequested(): Boolean { + return true + } - super.onPause() + override fun viewToInvalidateTimeout(): View? { + return coordinatorLayout } - private fun fillEntryDataInContentsView(entry: Entry) { + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) - val entryInfo = entry.getEntryInfo(mDatabase) + mEntryViewModel.loadDatabase(database) // Assign title icon - titleIconView?.let { iconView -> - mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconView, entryInfo.icon, iconColor) + mIcon?.let { icon -> + titleIconView?.let { iconView -> + mIconDrawableFactory?.assignDatabaseIcon(iconView, icon, mIconColor) + } } + } - // Assign title text - val entryTitle = entryInfo.title - collapsingToolbarLayout?.title = entryTitle - toolbar?.title = entryTitle - - // Assign basic fields - entryContentsView?.assignUserName(entryInfo.username) { - clipboardHelper?.timeoutCopyToClipboard(entryInfo.username, - getString(R.string.copy_field, - getString(R.string.entry_user_name))) + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + when (actionTask) { + ACTION_DATABASE_RESTORE_ENTRY_HISTORY, + ACTION_DATABASE_DELETE_ENTRY_HISTORY -> { + // Close the current activity after an history action + if (result.isSuccess) + finish() + } } + coordinatorLayout?.showActionErrorIfNeeded(result) + } - val isFirstTimeAskAllowCopyPasswordAndProtectedFields = - PreferencesUtil.isFirstTimeAskAllowCopyPasswordAndProtectedFields(this) - val allowCopyPasswordAndProtectedFields = - PreferencesUtil.allowCopyPasswordAndProtectedFields(this) - - val showWarningClipboardDialogOnClickListener = View.OnClickListener { - AlertDialog.Builder(this@EntryActivity) - .setMessage(getString(R.string.allow_copy_password_warning) + - "\n\n" + - getString(R.string.clipboard_warning)) - .create().apply { - setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ -> - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, true) - dialog.dismiss() - fillEntryDataInContentsView(entry) - } - setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ -> - PreferencesUtil.setAllowCopyPasswordAndProtectedFields(this@EntryActivity, false) - dialog.dismiss() - fillEntryDataInContentsView(entry) - } - show() - } - } + override fun onResume() { + super.onResume() - val onPasswordCopyClickListener: View.OnClickListener? = if (allowCopyPasswordAndProtectedFields) { - View.OnClickListener { - clipboardHelper?.timeoutCopyToClipboard(entryInfo.password, - getString(R.string.copy_field, - getString(R.string.entry_password))) - } + // Show the lock button + lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { + View.VISIBLE } else { - // If dialog not already shown - if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { - showWarningClipboardDialogOnClickListener - } else { - null - } - } - entryContentsView?.assignPassword(entryInfo.password, - allowCopyPasswordAndProtectedFields, - onPasswordCopyClickListener) - - //Assign OTP field - entry.getOtpElement()?.let { otpElement -> - entryContentsView?.assignOtp(otpElement, entryProgress) { - clipboardHelper?.timeoutCopyToClipboard( - otpElement.token, - getString(R.string.copy_field, getString(R.string.entry_otp)) - ) - } + View.GONE } - entryContentsView?.assignURL(entryInfo.url) - entryContentsView?.assignNotes(entryInfo.notes) - - // Assign custom fields - if (mDatabase?.allowEntryCustomFields() == true) { - entryContentsView?.clearExtraFields() - entryInfo.customFields.forEach { field -> - val label = field.name - // OTP field is already managed in dedicated view - if (label != OtpEntryFields.OTP_TOKEN_FIELD) { - val value = field.protectedValue - val allowCopyProtectedField = !value.isProtected || allowCopyPasswordAndProtectedFields - if (allowCopyProtectedField) { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField) { - clipboardHelper?.timeoutCopyToClipboard( - value.toString(), - getString(R.string.copy_field, label) - ) - } - } else { - // If dialog not already shown - if (isFirstTimeAskAllowCopyPasswordAndProtectedFields) { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField, showWarningClipboardDialogOnClickListener) - } else { - entryContentsView?.addExtraField(label, value, allowCopyProtectedField, null) - } - } + mAttachmentFileBinderManager?.apply { + registerProgressTask() + onActionTaskListener = object : AttachmentFileNotificationService.ActionTaskListener { + override fun onAttachmentAction(fileUri: Uri, entryAttachmentState: EntryAttachmentState) { + mEntryViewModel.onAttachmentAction(entryAttachmentState) } } } - entryContentsView?.setHiddenProtectedValue(!mShowPassword) - - // Manage attachments - entryContentsView?.assignAttachments(entryInfo.attachments.toSet(), StreamDirection.DOWNLOAD) { attachmentItem -> - mExternalFileHelper?.createDocument(attachmentItem.name)?.let { requestCode -> - mAttachmentsToDownload[requestCode] = attachmentItem - } - } + } - // Assign dates - entryContentsView?.assignCreationDate(entryInfo.creationTime) - entryContentsView?.assignModificationDate(entryInfo.lastModificationTime) - entryContentsView?.setExpires(entryInfo.expires, entryInfo.expiryTime) - - // Manage history - historyView?.visibility = if (mIsHistory) View.VISIBLE else View.GONE - if (mIsHistory) { - val taColorAccent = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent)) - collapsingToolbarLayout?.contentScrim = ColorDrawable(taColorAccent.getColor(0, Color.BLACK)) - taColorAccent.recycle() - } - entryContentsView?.assignHistory(entry.getHistory()) { historyItem, position -> - launch(this, historyItem, mReadOnly, position) - } + override fun onPause() { + mAttachmentFileBinderManager?.unregisterProgressTask() - // Assign special data - entryContentsView?.assignUUID(entry.nodeId.id) + super.onPause() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { - EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> - // Not directly get the entry from intent data but from database - mEntry?.let { - fillEntryDataInContentsView(it) - } + EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { + // Reload the current id from database + mEntryViewModel.loadDatabase(mDatabase) + } } mExternalFileHelper?.onCreateDocumentResult(requestCode, resultCode, data) { createdFileUri -> @@ -398,56 +312,57 @@ class EntryActivity : LockingActivity() { override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) + if (mEntryLoaded) { + val inflater = menuInflater + MenuUtil.contributionMenuInflater(inflater, menu) - val inflater = menuInflater - MenuUtil.contributionMenuInflater(inflater, menu) - inflater.inflate(R.menu.entry, menu) - inflater.inflate(R.menu.database, menu) - if (mIsHistory && !mReadOnly) { - inflater.inflate(R.menu.entry_history, menu) - } - if (mIsHistory || mReadOnly) { - menu.findItem(R.id.menu_save_database)?.isVisible = false - menu.findItem(R.id.menu_edit)?.isVisible = false - } - if (mSpecialMode != SpecialMode.DEFAULT) { - menu.findItem(R.id.menu_reload_database)?.isVisible = false - } + inflater.inflate(R.menu.entry, menu) + inflater.inflate(R.menu.database, menu) - val gotoUrl = menu.findItem(R.id.menu_goto_url) - gotoUrl?.apply { - // In API >= 11 onCreateOptionsMenu may be called before onCreate completes - // so mEntry may not be set - if (mEntry == null) { - isVisible = false - } else { - if (mEntry?.url?.isEmpty() != false) { - // disable button if url is not available - isVisible = false - } + if (mEntryIsHistory && !mDatabaseReadOnly) { + inflater.inflate(R.menu.entry_history, menu) } - } - - // Show education views - Handler(Looper.getMainLooper()).post { performedNextEducation(EntryActivityEducation(this), menu) } + // Show education views + Handler(Looper.getMainLooper()).post { + performedNextEducation( + EntryActivityEducation( + this + ), menu + ) + } + } return true } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + if (mUrl?.isEmpty() != false) { + menu?.findItem(R.id.menu_goto_url)?.isVisible = false + } + if (mEntryIsHistory || mDatabaseReadOnly) { + menu?.findItem(R.id.menu_save_database)?.isVisible = false + menu?.findItem(R.id.menu_edit)?.isVisible = false + } + if (mSpecialMode != SpecialMode.DEFAULT) { + menu?.findItem(R.id.menu_reload_database)?.isVisible = false + } + return super.onPrepareOptionsMenu(menu) + } + private fun performedNextEducation(entryActivityEducation: EntryActivityEducation, menu: Menu) { - val entryFieldCopyView = entryContentsView?.firstEntryFieldCopyView() + val entryFragment = supportFragmentManager.findFragmentByTag(ENTRY_FRAGMENT_TAG) + as? EntryFragment? + val entryFieldCopyView: View? = entryFragment?.firstEntryFieldCopyView() val entryCopyEducationPerformed = entryFieldCopyView != null && entryActivityEducation.checkAndPerformedEntryCopyEducation( - entryFieldCopyView, - { - val appNameString = getString(R.string.app_name) - clipboardHelper?.timeoutCopyToClipboard(appNameString, - getString(R.string.copy_field, appNameString)) - }, - { - performedNextEducation(entryActivityEducation, menu) - }) + entryFieldCopyView, + { + entryFragment.launchEntryCopyEducationAction() + }, + { + performedNextEducation(entryActivityEducation, menu) + }) if (!entryCopyEducationPerformed) { val menuEditView = toolbar?.findViewById(R.id.menu_edit) @@ -471,60 +386,53 @@ class EntryActivity : LockingActivity() { return true } R.id.menu_edit -> { - mEntry?.let { - EntryEditActivity.launch(this@EntryActivity, it) + mDatabase?.let { database -> + mMainEntryId?.let { entryId -> + EntryEditActivity.launchToUpdate( + this, + database, + entryId + ) + } } return true } R.id.menu_goto_url -> { - var url: String = mEntry?.url ?: "" - - // Default http:// if no protocol specified - if (!url.contains("://")) { - url = "http://$url" + mUrl?.let { url -> + UriUtil.gotoUrl(this, url) } - - UriUtil.gotoUrl(this, url) return true } R.id.menu_restore_entry_history -> { - mEntryLastVersion?.let { mainEntry -> - mProgressDatabaseTaskProvider?.startDatabaseRestoreEntryHistory( - mainEntry, - mEntryHistoryPosition, - !mReadOnly && mAutoSaveEnable) + mMainEntryId?.let { mainEntryId -> + restoreEntryHistory( + mainEntryId, + mHistoryPosition) } } R.id.menu_delete_entry_history -> { - mEntryLastVersion?.let { mainEntry -> - mProgressDatabaseTaskProvider?.startDatabaseDeleteEntryHistory( - mainEntry, - mEntryHistoryPosition, - !mReadOnly && mAutoSaveEnable) + mMainEntryId?.let { mainEntryId -> + deleteEntryHistory( + mainEntryId, + mHistoryPosition) } } R.id.menu_save_database -> { - mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) + saveDatabase() } R.id.menu_reload_database -> { - mProgressDatabaseTaskProvider?.startDatabaseReload(false) + reloadDatabase() } android.R.id.home -> finish() // close this activity and return to preview activity (if there is any) } return super.onOptionsItemSelected(item) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - - outState.putBoolean(KEY_FIRST_LAUNCH_ACTIVITY, mFirstLaunchOfActivity) - } - override fun finish() { // Transit data in previous Activity after an update Intent().apply { - putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mEntry) - setResult(EntryEditActivity.UPDATE_ENTRY_RESULT_CODE, this) + putExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY, mMainEntryId) + setResult(EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE, this) } super.finish() } @@ -532,19 +440,46 @@ class EntryActivity : LockingActivity() { companion object { private val TAG = EntryActivity::class.java.name - private const val KEY_FIRST_LAUNCH_ACTIVITY = "KEY_FIRST_LAUNCH_ACTIVITY" - const val KEY_ENTRY = "KEY_ENTRY" const val KEY_ENTRY_HISTORY_POSITION = "KEY_ENTRY_HISTORY_POSITION" - fun launch(activity: Activity, entry: Entry, readOnly: Boolean, historyPosition: Int? = null) { - if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { - val intent = Intent(activity, EntryActivity::class.java) - intent.putExtra(KEY_ENTRY, entry.nodeId) - ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) - if (historyPosition != null) + const val ENTRY_FRAGMENT_TAG = "ENTRY_FRAGMENT_TAG" + + /** + * Open standard Entry activity + */ + fun launch(activity: Activity, + database: Database, + entryId: NodeId) { + if (database.loaded) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryActivity::class.java) + intent.putExtra(KEY_ENTRY, entryId) + activity.startActivityForResult( + intent, + EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE + ) + } + } + } + + /** + * Open history Entry activity + */ + fun launch(activity: Activity, + database: Database, + entryId: NodeId, + historyPosition: Int) { + if (database.loaded) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryActivity::class.java) + intent.putExtra(KEY_ENTRY, entryId) intent.putExtra(KEY_ENTRY_HISTORY_POSITION, historyPosition) - activity.startActivityForResult(intent, EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE) + activity.startActivityForResult( + intent, + EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE + ) + } } } } 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 425f54e4f..adff0943d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -32,8 +32,8 @@ 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 android.widget.* +import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -46,39 +46,35 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani 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.activities.legacy.DatabaseLockActivity +import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter 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.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 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.ActionRunnable 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 com.kunzisoft.keepass.view.* +import com.kunzisoft.keepass.viewmodels.EntryEditViewModel import org.joda.time.DateTime import java.util.* import kotlin.collections.ArrayList -class EntryEditActivity : LockingActivity(), +class EntryEditActivity : DatabaseLockActivity(), EntryCustomFieldDialogFragment.EntryCustomFieldListener, GeneratePasswordDialogFragment.GeneratePasswordListener, SetOTPDialogFragment.CreateOtpListener, @@ -87,27 +83,26 @@ class EntryEditActivity : LockingActivity(), 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 templateSelectorSpinner: Spinner? = null private var entryEditAddToolBar: ToolbarAction? = null private var validateButton: View? = null private var lockView: View? = null + private var loadingView: ProgressBar? = null + + private val mEntryEditViewModel: EntryEditViewModel by viewModels() + private var mTemplate: Template? = null + private var mIsTemplate: Boolean = false + private var mEntryLoaded: Boolean = false + + private var mAllowCustomFields = false + private var mAllowOTP = false // 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 @@ -124,218 +119,287 @@ class EntryEditActivity : LockingActivity(), 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 - + templateSelectorSpinner = findViewById(R.id.entry_edit_template_selector) lockView = findViewById(R.id.lock_button) - lockView?.setOnClickListener { - lockAndExit() - } - - // Focus view to reinitialize timeout - coordinatorLayout?.resetAppTimeoutWhenViewFocusedOrChanged(this) + validateButton = findViewById(R.id.entry_edit_validate) + loadingView = findViewById(R.id.loading) 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>(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) + var entryId: NodeId? = null + intent.getParcelableExtra>(KEY_ENTRY)?.let { entryToUpdate -> + intent.removeExtra(KEY_ENTRY) + entryId = entryToUpdate } // Parent is retrieve, it's a new entry to create - intent.getParcelableExtra>(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") + var parentId: NodeId<*>? = null + intent.getParcelableExtra>(KEY_PARENT)?.let { parent -> + intent.removeExtra(KEY_PARENT) + parentId = parent + } + + mEntryEditViewModel.loadTemplateEntry( + mDatabase, + entryId, + parentId, + EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent), + EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) + ) + + // To retrieve attachment + mExternalFileHelper = ExternalFileHelper(this) + mAttachmentFileBinderManager = AttachmentFileBinderManager(this) + // Verify the education views + entryEditActivityEducation = EntryEditActivityEducation(this) + + // Lock button + lockView?.setOnClickListener { lockAndExit() } + // Save button + validateButton?.setOnClickListener { saveEntry() } + + mEntryEditViewModel.onTemplateChanged.observe(this) { template -> + this.mTemplate = template + } + + mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry -> + if (templatesEntry != null) { + // Change template dynamically + this.mIsTemplate = templatesEntry.isTemplate + templatesEntry.templates.let { templates -> + templateSelectorSpinner?.apply { + // Build template selector + if (templates.isNotEmpty()) { + adapter = TemplatesSelectorAdapter( + this@EntryEditActivity, + mIconDrawableFactory, + templates + ) + val selectedTemplate = if (mTemplate != null) + mTemplate + else + templatesEntry.defaultTemplate + setSelection(templates.indexOf(selectedTemplate)) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + mEntryEditViewModel.changeTemplate(templates[position]) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + } else { + visibility = View.GONE + } + } } + + loadingView?.hideByFading() + mEntryLoaded = true + } else { + finish() } - setOnPasswordGeneratorClickListener = View.OnClickListener { - openPasswordGenerator() + invalidateOptionsMenu() + } + + // View model listeners + mEntryEditViewModel.requestIconSelection.observe(this) { iconImage -> + IconPickerActivity.launch(this@EntryEditActivity, iconImage) + } + + mEntryEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> + if (dateInstant.type == DateInstant.Type.TIME) { + // Launch the time picker + val dateTime = DateTime(dateInstant.date) + TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour) + .show(supportFragmentManager, "TimePickerFragment") + } else { + // Launch the date picker + val dateTime = DateTime(dateInstant.date) + DatePickerFragment.getInstance(dateTime.year, dateTime.monthOfYear - 1, dateTime.dayOfMonth) + .show(supportFragmentManager, "DatePickerFragment") } - // Add listener to the icon - setOnIconViewClickListener = { iconImage -> - IconPickerActivity.launch(this@EntryEditActivity, iconImage) + } + + 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() } - setOnRemoveAttachment = { attachment -> - mAttachmentFileBinderManager?.removeBinaryAttachment(attachment) - removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD)) + } + + mEntryEditViewModel.onStartUploadAttachment.observe(this) { + // Start uploading in service + mAttachmentFileBinderManager?.startUploadAttachment(it.attachmentToUploadUri, it.attachment) + } + + mEntryEditViewModel.onAttachmentAction.observe(this) { attachmentState -> + when (attachmentState?.downloadState) { + AttachmentState.ERROR -> { + coordinatorLayout?.let { + Snackbar.make(it, R.string.error_file_not_create, Snackbar.LENGTH_LONG).asError().show() + } + } + else -> {} } - setOnEditCustomField = { field -> - editCustomField(field) + } + + mEntryEditViewModel.onBinaryPreviewLoaded.observe(this) { + // Scroll to the attachment position + when (it.entryAttachmentState.downloadState) { + AttachmentState.START, + AttachmentState.COMPLETE -> { + scrollView?.smoothScrollTo(0, it.viewPosition.toInt()) + } + else -> {} } } - // Retrieve temp attachments in case of deletion - if (savedInstanceState?.containsKey(TEMP_ATTACHMENTS) == true) { - mTempAttachments = savedInstanceState.getParcelableArrayList(TEMP_ATTACHMENTS) ?: mTempAttachments + mEntryEditViewModel.attachmentDeleted.observe(this) { + mAttachmentFileBinderManager?.removeBinaryAttachment(it) } - // To retrieve attachment - mExternalFileHelper = ExternalFileHelper(this) - mAttachmentFileBinderManager = AttachmentFileBinderManager(this) + // Build new entry from the entry info retrieved + mEntryEditViewModel.onEntrySaved.observe(this) { entrySave -> + // Open a progress dialog and save entry + entrySave.parent?.let { parent -> + createEntry(entrySave.newEntry, parent) + } ?: run { + updateEntry(entrySave.oldEntry, entrySave.newEntry) + } - // Save button - validateButton = findViewById(R.id.entry_edit_validate) - validateButton?.setOnClickListener { saveEntry() } + // Don't wait for saving if it's to provide autofill + mDatabase?.let { database -> + EntrySelectionHelper.doSpecialAction(intent, + {}, + {}, + {}, + { + entryValidatedForKeyboardSelection(database, entrySave.newEntry) + }, + { _, _ -> + entryValidatedForAutofillSelection(database, entrySave.newEntry) + }, + { + entryValidatedForAutofillRegistration(entrySave.newEntry) + } + ) + } + } + } - // Verify the education views - entryEditActivityEducation = EntryEditActivityEducation(this) + override fun viewToInvalidateTimeout(): View? { + return coordinatorLayout + } - // 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 = 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() - } - ) - } + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + mAllowCustomFields = database?.allowEntryCustomFields() == true + mAllowOTP = database?.allowOTP == true + mEntryEditViewModel.loadDatabase(database) + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + when (actionTask) { + ACTION_DATABASE_CREATE_ENTRY_TASK, + ACTION_DATABASE_UPDATE_ENTRY_TASK -> { + try { + if (result.isSuccess) { + var newNodes: List = ArrayList() + result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle -> + newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle) + } + if (newNodes.size == 1) { + (newNodes[0] as? Entry?)?.let { entry -> + EntrySelectionHelper.doSpecialAction(intent, + { + // Finish naturally + finishForEntryResult(entry) + }, + { + // Nothing when search retrieved + }, + { + entryValidatedForSave(entry) + }, + { + entryValidatedForKeyboardSelection(database, entry) + }, + { _, _ -> + entryValidatedForAutofillSelection(database, entry) + }, + { + entryValidatedForAutofillRegistration(entry) + } + ) } } - } 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() + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve entry after database action", e) } } - coordinatorLayout?.showActionErrorIfNeeded(result) } + coordinatorLayout?.showActionErrorIfNeeded(result) } - private fun entryValidatedForSave() { + private fun entryValidatedForSave(entry: Entry) { onValidateSpecialMode() - finishForEntryResult() + finishForEntryResult(entry) } - private fun entryValidatedForKeyboardSelection(entry: Entry) { + private fun entryValidatedForKeyboardSelection(database: Database, entry: Entry) { // Populate Magikeyboard with entry - mDatabase?.let { database -> - populateKeyboardAndMoveAppToBackground(this, - entry.getEntryInfo(database), - intent) - } + populateKeyboardAndMoveAppToBackground(this, + entry.getEntryInfo(database), + intent) onValidateSpecialMode() // Don't keep activity history for entry edition - finishForEntryResult() + finishForEntryResult(entry) } - private fun entryValidatedForAutofillSelection(entry: Entry) { + private fun entryValidatedForAutofillSelection(database: Database, 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)) - } + AutofillHelper.buildResponseAndSetResult(this@EntryEditActivity, + database, + entry.getEntryInfo(database)) } onValidateSpecialMode() } - private fun entryValidatedForAutofillRegistration() { + private fun entryValidatedForAutofillRegistration(entry: Entry) { onValidateSpecialMode() - finishForEntryResult() + finishForEntryResult(entry) } override fun onResume() { @@ -350,43 +414,11 @@ class EntryEditActivity : LockingActivity(), // 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 -> {} - } + mEntryEditViewModel.onAttachmentAction(entryAttachmentState) } } } @@ -398,13 +430,6 @@ class EntryEditActivity : LockingActivity(), super.onPause() } - /** - * Open the password generator fragment - */ - private fun openPasswordGenerator() { - GeneratePasswordDialogFragment().show(supportFragmentManager, "PasswordGeneratorFragment") - } - /** * Add a new customized field */ @@ -416,43 +441,16 @@ class EntryEditActivity : LockingActivity(), 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) - } + mEntryEditViewModel.addCustomField(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) - } - } + mEntryEditViewModel.editCustomField(oldField, newField) } override fun onDeleteCustomFieldApproved(oldField: Field) { - entryEditFragment?.removeExtraField(oldField) + mEntryEditViewModel.removeCustomField(oldField) } /** @@ -464,37 +462,13 @@ class EntryEditActivity : LockingActivity(), override fun onValidateUploadFileTooBig(attachmentToUploadUri: Uri?, fileName: String?) { if (attachmentToUploadUri != null && fileName != null) { - buildNewAttachment(attachmentToUploadUri, fileName) + mEntryEditViewModel.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) - } + mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, attachment) } } @@ -502,7 +476,7 @@ class EntryEditActivity : LockingActivity(), super.onActivityResult(requestCode, resultCode, data) IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> - entryEditFragment?.icon = icon + mEntryEditViewModel.selectIcon(icon) } mExternalFileHelper?.onOpenDocumentResult(requestCode, resultCode, data) { uri -> @@ -513,7 +487,7 @@ class EntryEditActivity : LockingActivity(), FileTooBigDialogFragment.build(attachmentToUploadUri, fileName) .show(supportFragmentManager, "fileTooBigFragment") } else { - buildNewAttachment(attachmentToUploadUri, fileName) + mEntryEditViewModel.buildNewAttachment(attachmentToUploadUri, fileName) } } } @@ -524,11 +498,12 @@ 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(entryEditFragment?.getEntryInfo()?.otpModel) - .show(supportFragmentManager, "addOTPDialog") + private fun setupOtp() { + mEntryEditViewModel.setupOtp() + } + + override fun onOtpCreated(otpElement: OtpElement) { + mEntryEditViewModel.createOtp(otpElement) } /** @@ -536,107 +511,62 @@ class EntryEditActivity : LockingActivity(), */ 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 - ) - } - } - } - } + mEntryEditViewModel.requestEntryInfoUpdate(mDatabase) } override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) - menuInflater.inflate(R.menu.entry_edit, menu) + if (mEntryLoaded) { + menuInflater.inflate(R.menu.entry_edit, menu) + entryEditActivityEducation?.let { + Handler(Looper.getMainLooper()).post { + performedNextEducation(it) + } + } + } 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 + isEnabled = mAllowCustomFields + isVisible = isEnabled } - - // 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_attachment)?.apply { + // Attachment not compatible below KitKat + isEnabled = !mIsTemplate + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + isVisible = isEnabled } - 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) } + isEnabled = mAllowOTP + && !mIsTemplate + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + isVisible = isEnabled } return super.onPrepareOptionsMenu(menu) } - fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { - if (entryEditFragment?.generatePasswordEducationPerformed(entryEditActivityEducation) != true) { + private fun performedNextEducation(entryEditActivityEducation: EntryEditActivityEducation) { + + val entryEditFragment = supportFragmentManager.findFragmentById(R.id.entry_edit_content) + as? EntryEditFragment? + val generatePasswordView = entryEditFragment?.getActionImageView() + val generatePasswordEductionPerformed = generatePasswordView != null + && entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( + generatePasswordView, + { + entryEditFragment.launchGeneratePasswordEductionAction() + }, + { + performedNextEducation(entryEditActivityEducation) + } + ) + + if (!generatePasswordEductionPerformed) { val addNewFieldView: View? = entryEditAddToolBar?.findViewById(R.id.menu_add_field) - val addNewFieldEducationPerformed = mDatabase?.allowEntryCustomFields() == true + val addNewFieldEducationPerformed = mAllowCustomFields && addNewFieldView != null && addNewFieldView.isVisible && entryEditActivityEducation.checkAndPerformedEntryNewFieldEducation( @@ -668,7 +598,7 @@ class EntryEditActivity : LockingActivity(), && entryEditActivityEducation.checkAndPerformedSetUpOTPEducation( setupOtpView, { - setupOTP() + setupOtp() } ) } @@ -687,7 +617,7 @@ class EntryEditActivity : LockingActivity(), return true } R.id.menu_add_otp -> { - setupOTP() + setupOtp() return true } android.R.id.home -> { @@ -698,77 +628,26 @@ class EntryEditActivity : LockingActivity(), 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") - } + mEntryEditViewModel.selectDate(year, month, day) } } 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()) - } + mEntryEditViewModel.selectTime(hours, minutes) } - 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 - } - + override fun acceptPassword(passwordField: Field) { + mEntryEditViewModel.selectPassword(passwordField) entryEditActivityEducation?.let { Handler(Looper.getMainLooper()).post { performedNextEducation(it) } } } - override fun cancelPassword(bundle: Bundle) { + override fun cancelPassword(passwordField: Field) { // Do nothing here } @@ -800,20 +679,14 @@ class EntryEditActivity : LockingActivity(), } } - private fun finishForEntryResult() { + private fun finishForEntryResult(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 (mIsNew) { - setResult(ADD_ENTRY_RESULT_CODE, intentEntry) - } else { - setResult(UPDATE_ENTRY_RESULT_CODE, intentEntry) - } - } + val bundle = Bundle() + val intentEntry = Intent() + bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId) + intentEntry.putExtras(bundle) + setResult(ADD_OR_UPDATE_ENTRY_RESULT_CODE, intentEntry) super.finish() } catch (e: Exception) { // Exception when parcelable can't be done @@ -829,68 +702,72 @@ 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 + const val ADD_OR_UPDATE_ENTRY_RESULT_CODE = 31 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 + * Launch EntryEditActivity to update an existing entry by his [entryId] */ - 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) + fun launchToUpdate(activity: Activity, + database: Database, + entryId: NodeId) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryEditActivity::class.java) + intent.putExtra(KEY_ENTRY, entryId) + 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 + * Launch EntryEditActivity to add a new entry in an existent group */ - 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 launchToCreate(activity: Activity, + database: Database, + groupId: NodeId<*>) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, groupId) + 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, + fun launchToUpdateForSave(context: Context, + database: Database, + entryId: NodeId, + searchInfo: SearchInfo) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { + val intent = Intent(context, EntryEditActivity::class.java) + intent.putExtra(KEY_ENTRY, entryId) + EntrySelectionHelper.startActivityForSaveModeResult( + context, intent, - searchInfo) + 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, + fun launchToCreateForSave(context: Context, + database: Database, + groupId: NodeId<*>, + searchInfo: SearchInfo) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { + val intent = Intent(context, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, groupId) + EntrySelectionHelper.startActivityForSaveModeResult( + context, intent, - searchInfo) + searchInfo + ) + } } } @@ -898,14 +775,19 @@ class EntryEditActivity : LockingActivity(), * Launch EntryEditActivity to add a new entry in keyboard selection */ fun launchForKeyboardSelectionResult(context: Context, - group: Group, + database: Database, + groupId: NodeId<*>, searchInfo: SearchInfo? = null) { - if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { - val intent = Intent(context, EntryEditActivity::class.java) - intent.putExtra(KEY_PARENT, group.nodeId) - EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context, + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { + val intent = Intent(context, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, groupId) + EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( + context, intent, - searchInfo) + searchInfo + ) + } } } @@ -914,46 +796,61 @@ class EntryEditActivity : LockingActivity(), */ @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, + database: Database, autofillComponent: AutofillComponent, - group: Group, + groupId: NodeId<*>, searchInfo: SearchInfo? = null) { - if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { - val intent = Intent(activity, EntryEditActivity::class.java) - intent.putExtra(KEY_PARENT, group.nodeId) - AutofillHelper.startActivityForAutofillResult(activity, + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { + val intent = Intent(activity, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, groupId) + AutofillHelper.startActivityForAutofillResult( + activity, intent, autofillComponent, - searchInfo) + 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, + fun launchToUpdateForRegistration(context: Context, + database: Database, + entryId: NodeId, + registerInfo: RegisterInfo? = null) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { + val intent = Intent(context, EntryEditActivity::class.java) + intent.putExtra(KEY_ENTRY, entryId) + EntrySelectionHelper.startActivityForRegistrationModeResult( + context, intent, - registerInfo) + 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, + fun launchToCreateForRegistration(context: Context, + database: Database, + groupId: NodeId<*>, + registerInfo: RegisterInfo? = null) { + if (database.loaded && !database.isReadOnly) { + if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) { + val intent = Intent(context, EntryEditActivity::class.java) + intent.putExtra(KEY_PARENT, groupId) + EntrySelectionHelper.startActivityForRegistrationModeResult( + context, intent, - registerInfo) + registerInfo + ) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt index 7927c502a..ad6f570db 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt @@ -22,14 +22,13 @@ package com.kunzisoft.keepass.activities import android.app.Activity import android.content.Intent import android.net.Uri -import android.os.Bundle import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.search.SearchHelper -import com.kunzisoft.keepass.magikeyboard.MagikIME +import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.otp.OtpEntryFields @@ -39,10 +38,18 @@ import com.kunzisoft.keepass.settings.PreferencesUtil * Activity to search or select entry in database, * Commonly used with Magikeyboard */ -class EntrySelectionLauncherActivity : AppCompatActivity() { +class EntrySelectionLauncherActivity : DatabaseModeActivity() { - override fun onCreate(savedInstanceState: Bundle?) { + override fun applyCustomStyle(): Boolean { + return false + } + + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) var sharedWebDomain: String? = null var otpString: String? = null @@ -68,39 +75,39 @@ class EntrySelectionLauncherActivity : AppCompatActivity() { else -> {} } - // Build domain search param val searchInfo = SearchInfo().apply { this.webDomain = sharedWebDomain this.otpString = otpString } + SearchInfo.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain -> searchInfo.webDomain = concreteWebDomain - launch(searchInfo) + launch(database, searchInfo) } - - super.onCreate(savedInstanceState) } - private fun launch(searchInfo: SearchInfo) { + private fun launch(database: Database?, + searchInfo: SearchInfo) { if (!searchInfo.containsOnlyNullValues()) { // Setting to integrate Magikeyboard val searchShareForMagikeyboard = PreferencesUtil.isKeyboardSearchShareEnable(this) // If database is open - val database = Database.getInstance() - val readOnly = database.isReadOnly + val readOnly = database?.isReadOnly != false SearchHelper.checkAutoSearchInfo(this, database, searchInfo, - { items -> + { openedDatabase, items -> // Items found if (searchInfo.otpString != null) { if (!readOnly) { - GroupActivity.launchForSaveResult(this, - searchInfo, - false) + GroupActivity.launchForSaveResult( + this, + openedDatabase, + searchInfo, + false) } else { Toast.makeText(applicationContext, R.string.autofill_read_only_save, @@ -111,30 +118,32 @@ class EntrySelectionLauncherActivity : AppCompatActivity() { if (items.size == 1) { // Automatically populate keyboard val entryPopulate = items[0] - populateKeyboardAndMoveAppToBackground(this, + populateKeyboardAndMoveAppToBackground( + this, entryPopulate, intent) } else { // Select the one we want GroupActivity.launchForKeyboardSelectionResult(this, - readOnly, - searchInfo, - true) + openedDatabase, + searchInfo, + true) } } else { GroupActivity.launchForSearchResult(this, - readOnly, - searchInfo, - true) + openedDatabase, + searchInfo, + true) } }, - { + { openedDatabase -> // Show the database UI to select the entry if (searchInfo.otpString != null) { if (!readOnly) { GroupActivity.launchForSaveResult(this, - searchInfo, - false) + openedDatabase, + searchInfo, + false) } else { Toast.makeText(applicationContext, R.string.autofill_read_only_save, @@ -143,13 +152,14 @@ class EntrySelectionLauncherActivity : AppCompatActivity() { } } else if (readOnly || searchShareForMagikeyboard) { GroupActivity.launchForKeyboardSelectionResult(this, - readOnly, - searchInfo, - false) + openedDatabase, + searchInfo, + false) } else { GroupActivity.launchForSaveResult(this, - searchInfo, - false) + openedDatabase, + searchInfo, + false) } }, { @@ -183,7 +193,7 @@ fun populateKeyboardAndMoveAppToBackground(activity: Activity, intent: Intent, toast: Boolean = true) { // Populate Magikeyboard with entry - MagikIME.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) + MagikeyboardService.addEntryAndLaunchNotificationIfAllowed(activity, entry, toast) // Consume the selection mode EntrySelectionHelper.removeModesFromIntent(intent) activity.moveTaskToBack(true) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt index 02500c8ce..d25b93964 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt @@ -45,12 +45,11 @@ import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener -import com.kunzisoft.keepass.activities.selection.SpecialModeActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper -import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.model.MainCredential @@ -61,12 +60,13 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion. import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import java.io.FileNotFoundException -class FileDatabaseSelectActivity : SpecialModeActivity(), +class FileDatabaseSelectActivity : DatabaseModeActivity(), AssignMasterKeyDialogFragment.AssignPasswordDialogListener { // Views @@ -85,8 +85,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), private var mExternalFileHelper: ExternalFileHelper? = null - private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -127,7 +125,6 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } mAdapterDatabaseHistory?.setOnFileDatabaseHistoryDeleteListener { fileDatabaseHistoryToDelete -> - // Remove from app database databaseFilesViewModel.deleteDatabaseFile(fileDatabaseHistoryToDelete) true } @@ -190,39 +187,62 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), // Retrieve settings for default database mAdapterDatabaseHistory?.setDefaultDatabase(it) } + } - // Attach the dialog thread to this activity - mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { - onActionFinish = { actionTask, result -> - when (actionTask) { - ACTION_DATABASE_CREATE_TASK -> { - result.data?.getParcelable(DATABASE_URI_KEY)?.let { databaseUri -> - val mainCredential = result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) ?: MainCredential() - databaseFilesViewModel.addDatabaseFile(databaseUri, mainCredential.keyFileUri) - } - GroupActivity.launch(this@FileDatabaseSelectActivity, - PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)) - } - ACTION_DATABASE_LOAD_TASK -> { - val database = Database.getInstance() - if (result.isSuccess - && database.loaded) { - launchGroupActivity(database) - } else { - var resultError = "" - val resultMessage = result.message - // Show error message - if (resultMessage != null && resultMessage.isNotEmpty()) { - resultError = "$resultError $resultMessage" - } - Log.e(TAG, resultError) - Snackbar.make(coordinatorLayout, - resultError, - Snackbar.LENGTH_LONG).asError().show() - } + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + if (database != null) { + launchGroupActivityIfLoaded(database) + } + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + + if (result.isSuccess) { + // Update list + when (actionTask) { + ACTION_DATABASE_CREATE_TASK, + ACTION_DATABASE_LOAD_TASK -> { + result.data?.getParcelable(DATABASE_URI_KEY)?.let { databaseUri -> + val mainCredential = + result.data?.getParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY) + ?: MainCredential() + databaseFilesViewModel.addDatabaseFile( + databaseUri, + mainCredential.keyFileUri + ) } } } + // Launch activity + when (actionTask) { + ACTION_DATABASE_CREATE_TASK -> { + GroupActivity.launch( + this@FileDatabaseSelectActivity, + database, + PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity) + ) + } + ACTION_DATABASE_LOAD_TASK -> { + launchGroupActivityIfLoaded(database) + } + } + } else { + var resultError = "" + val resultMessage = result.message + // Show error message + if (resultMessage != null && resultMessage.isNotEmpty()) { + resultError = "$resultError $resultMessage" + } + Log.e(TAG, resultError) + Snackbar.make(coordinatorLayout, + resultError, + Snackbar.LENGTH_LONG).asError().show() } } @@ -251,12 +271,14 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), { onLaunchActivitySpecialMode() }) } - private fun launchGroupActivity(database: Database) { - GroupActivity.launch(this, - database.isReadOnly, + private fun launchGroupActivityIfLoaded(database: Database) { + if (database.loaded) { + GroupActivity.launch(this, + database, { onValidateSpecialMode() }, { onCancelSpecialMode() }, { onLaunchActivitySpecialMode() }) + } } override fun onValidateSpecialMode() { @@ -296,28 +318,16 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } } - val database = Database.getInstance() - if (database.loaded) { - launchGroupActivity(database) - } else { - // Construct adapter with listeners - if (PreferencesUtil.showRecentFiles(this)) { - databaseFilesViewModel.loadListOfDatabases() - } else { - mAdapterDatabaseHistory?.clearDatabaseFileHistoryList() - mAdapterDatabaseHistory?.notifyDataSetChanged() - } - - // Register progress task - mProgressDatabaseTaskProvider?.registerProgressTask() + mDatabase?.let { database -> + launchGroupActivityIfLoaded(database) } - } - override fun onPause() { - // Unregister progress task - mProgressDatabaseTaskProvider?.unregisterProgressTask() - - super.onPause() + // Show recent files if allowed + if (PreferencesUtil.showRecentFiles(this@FileDatabaseSelectActivity)) { + databaseFilesViewModel.loadListOfDatabases() + } else { + mAdapterDatabaseHistory?.clearDatabaseFileHistoryList() + } } override fun onSaveInstanceState(outState: Bundle) { @@ -329,15 +339,10 @@ class FileDatabaseSelectActivity : SpecialModeActivity(), } override fun onAssignKeyDialogPositiveClick(mainCredential: MainCredential) { - try { mDatabaseFileUri?.let { databaseUri -> - // Create the new database - mProgressDatabaseTaskProvider?.startDatabaseCreate( - databaseUri, - mainCredential - ) + createDatabase(databaseUri, mainCredential) } } catch (e: Exception) { val error = getString(R.string.error_create_database_file) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt index db5c0011f..ad25c95b7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt @@ -26,31 +26,25 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.graphics.Color -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.* import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* +import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.fragment.app.FragmentManager -import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.* -import com.kunzisoft.keepass.activities.fragments.ListNodesFragment +import com.kunzisoft.keepass.activities.fragments.GroupFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper -import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.adapters.SearchEntryCursorAdapter import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper @@ -63,30 +57,24 @@ import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_GROUP_TASK -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_DELETE_NODES_TASK -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_MOVE_NODES_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.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_GROUP_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.OLD_NODES_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.view.* +import com.kunzisoft.keepass.viewmodels.GroupEditViewModel +import com.kunzisoft.keepass.viewmodels.GroupViewModel import org.joda.time.DateTime -class GroupActivity : LockingActivity(), - GroupEditDialogFragment.EditGroupListener, +class GroupActivity : DatabaseLockActivity(), DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener, - ListNodesFragment.NodeClickListener, - ListNodesFragment.NodesActionMenuListener, - DeleteNodesDialogFragment.DeleteNodeListener, - ListNodesFragment.OnScrollListener, + GroupFragment.NodeClickListener, + GroupFragment.NodesActionMenuListener, + GroupFragment.OnScrollListener, SortDialogFragment.SortSelectionListener { // Views @@ -100,31 +88,34 @@ class GroupActivity : LockingActivity(), private var numberChildrenView: TextView? = null private var addNodeButtonView: AddNodeButtonView? = null private var groupNameView: TextView? = null + private var groupMetaView: TextView? = null + private var loadingView: ProgressBar? = null - private var mDatabase: Database? = null + private val mGroupViewModel: GroupViewModel by viewModels() + private val mGroupEditViewModel: GroupEditViewModel by viewModels() - private var mListNodesFragment: ListNodesFragment? = null + private var mGroupFragment: GroupFragment? = null + private var mRecyclingBinEnabled = false + private var mRecyclingBinIsCurrentGroup = false private var mRequestStartupSearch = true private var actionNodeMode: ActionMode? = null - // To manage history in selection mode - private var mSelectionModeCountBackStack = 0 - // Nodes + private var mCurrentGroupState: GroupState? = null private var mRootGroup: Group? = null private var mCurrentGroup: Group? = null + private var mPreviousGroupsIds = mutableListOf() private var mOldGroupToUpdate: Group? = null private var mSearchSuggestionAdapter: SearchEntryCursorAdapter? = null + private var mOnSuggestionListener: SearchView.OnSuggestionListener? = null private var mIconColor: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mDatabase = Database.getInstance() - // Construct main view setContentView(layoutInflater.inflate(R.layout.activity_group, null)) @@ -137,8 +128,10 @@ class GroupActivity : LockingActivity(), toolbar = findViewById(R.id.toolbar) searchTitleView = findViewById(R.id.search_title) groupNameView = findViewById(R.id.group_name) + groupMetaView = findViewById(R.id.group_meta) toolbarAction = findViewById(R.id.toolbar_action) lockView = findViewById(R.id.lock_button) + loadingView = findViewById(R.id.loading) lockView?.setOnClickListener { lockAndExit() @@ -152,314 +145,330 @@ class GroupActivity : LockingActivity(), mIconColor = taTextColor.getColor(0, Color.WHITE) taTextColor.recycle() - // Focus view to reinitialize timeout - rootContainerView?.resetAppTimeoutWhenViewFocusedOrChanged(this) + // Retrieve group if defined at launch + manageIntent(intent) // Retrieve elements after an orientation change if (savedInstanceState != null) { - if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY)) + if (savedInstanceState.containsKey(REQUEST_STARTUP_SEARCH_KEY)) { mRequestStartupSearch = savedInstanceState.getBoolean(REQUEST_STARTUP_SEARCH_KEY) - if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) + savedInstanceState.remove(REQUEST_STARTUP_SEARCH_KEY) + } + if (savedInstanceState.containsKey(OLD_GROUP_TO_UPDATE_KEY)) { mOldGroupToUpdate = savedInstanceState.getParcelable(OLD_GROUP_TO_UPDATE_KEY) + savedInstanceState.remove(OLD_GROUP_TO_UPDATE_KEY) + } } - try { - mRootGroup = mDatabase?.rootGroup - } catch (e: NullPointerException) { - Log.e(TAG, "Unable to get rootGroup") - } - - mCurrentGroup = retrieveCurrentGroup(intent, savedInstanceState) - val currentGroupIsASearch = mCurrentGroup?.isVirtual == true - - Log.i(TAG, "Started creating tree") - if (mCurrentGroup == null) { - Log.w(TAG, "Group was null") - return + // Retrieve previous groups + if (savedInstanceState != null && savedInstanceState.containsKey(PREVIOUS_GROUPS_IDS_KEY)) { + try { + mPreviousGroupsIds = + (savedInstanceState.getParcelableArray(PREVIOUS_GROUPS_IDS_KEY) + ?.map { it as GroupState })?.toMutableList() ?: mutableListOf() + } catch (e: Exception) { + Log.e(TAG, "Unable to retrieve previous groups", e) + } + savedInstanceState.remove(PREVIOUS_GROUPS_IDS_KEY) } - var fragmentTag = LIST_NODES_FRAGMENT_TAG - if (currentGroupIsASearch) - fragmentTag = SEARCH_FRAGMENT_TAG - // Initialize the fragment with the list - mListNodesFragment = supportFragmentManager.findFragmentByTag(fragmentTag) as ListNodesFragment? - if (mListNodesFragment == null) - mListNodesFragment = ListNodesFragment.newInstance(mCurrentGroup, mReadOnly, currentGroupIsASearch) + mGroupFragment = + supportFragmentManager.findFragmentByTag(GROUP_FRAGMENT_TAG) as GroupFragment? + if (mGroupFragment == null) + mGroupFragment = GroupFragment() // Attach fragment to content view supportFragmentManager.beginTransaction().replace( - R.id.nodes_list_fragment_container, - mListNodesFragment!!, - fragmentTag) - .commit() - - // Update last access time. - mCurrentGroup?.touch(modified = false, touchParents = false) - - // To relaunch the activity with ACTION_SEARCH - if (manageSearchInfoIntent(intent)) { - startActivity(intent) - } - - // Add listeners to the add buttons - addNodeButtonView?.setAddGroupClickListener { - GroupEditDialogFragment.create(GroupInfo().apply { - if (mCurrentGroup?.allowAddNoteInGroup == true) { - notes = "" + R.id.nodes_list_fragment_container, + mGroupFragment!!, + GROUP_FRAGMENT_TAG + ).commit() + + // Observe group + mGroupViewModel.group.observe(this) { + val currentGroup = it.group + + mCurrentGroup = currentGroup + mRecyclingBinIsCurrentGroup = it.isRecycleBin + + if (!currentGroup.isVirtual) { + // Save group id if real group + mCurrentGroupState = GroupState(currentGroup.nodeId, it.showFromPosition) + + // Update last access time. + currentGroup.touch(modified = false, touchParents = false) + + // Add listeners to the add buttons + addNodeButtonView?.setAddGroupClickListener { + GroupEditDialogFragment.create(GroupInfo().apply { + if (currentGroup.allowAddNoteInGroup) { + notes = "" + } + }).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) } - }).show(supportFragmentManager, GroupEditDialogFragment.TAG_CREATE_GROUP) - } - addNodeButtonView?.setAddEntryClickListener { - mCurrentGroup?.let { currentGroup -> - EntrySelectionHelper.doSpecialAction(intent, - { - EntryEditActivity.launch(this@GroupActivity, currentGroup) - }, - { - // Search not used - }, - { searchInfo -> - EntryEditActivity.launchForSave(this@GroupActivity, - currentGroup, searchInfo) - onLaunchActivitySpecialMode() - }, - { searchInfo -> - EntryEditActivity.launchForKeyboardSelectionResult(this@GroupActivity, - currentGroup, searchInfo) - onLaunchActivitySpecialMode() - }, - { searchInfo, autofillComponent -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - EntryEditActivity.launchForAutofillResult(this@GroupActivity, + addNodeButtonView?.setAddEntryClickListener { + mDatabase?.let { database -> + EntrySelectionHelper.doSpecialAction(intent, + { + EntryEditActivity.launchToCreate( + this@GroupActivity, + database, + currentGroup.nodeId + ) + }, + { + // Search not used + }, + { searchInfo -> + EntryEditActivity.launchToCreateForSave( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + }, + { searchInfo -> + EntryEditActivity.launchForKeyboardSelectionResult( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + }, + { searchInfo, autofillComponent -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + EntryEditActivity.launchForAutofillResult( + this@GroupActivity, + database, autofillComponent, - currentGroup, searchInfo) + currentGroup.nodeId, + searchInfo + ) + onLaunchActivitySpecialMode() + } else { + onCancelSpecialMode() + } + }, + { searchInfo -> + EntryEditActivity.launchToCreateForRegistration( + this@GroupActivity, + database, + currentGroup.nodeId, + searchInfo + ) onLaunchActivitySpecialMode() - } else { - onCancelSpecialMode() } - }, - { searchInfo -> - EntryEditActivity.launchForRegistration(this@GroupActivity, - currentGroup, searchInfo) - onLaunchActivitySpecialMode() - } - ) + ) + } + } } + + assignGroupViewElements(currentGroup) + invalidateOptionsMenu() + + loadingView?.hideByFading() } - mDatabase?.let { database -> - // Search suggestion - mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, database) + mGroupViewModel.firstPositionVisible.observe(this) { firstPositionVisible -> + mCurrentGroupState?.firstVisibleItem = firstPositionVisible + } + + mGroupEditViewModel.requestIconSelection.observe(this) { iconImage -> + IconPickerActivity.launch(this@GroupActivity, iconImage) + } - // Init dialog thread - mProgressDatabaseTaskProvider?.onActionFinish = { actionTask, result -> + mGroupEditViewModel.requestDateTimeSelection.observe(this) { dateInstant -> + if (dateInstant.type == DateInstant.Type.TIME) { + // Launch the time picker + val dateTime = DateTime(dateInstant.date) + TimePickerFragment.getInstance(dateTime.hourOfDay, dateTime.minuteOfHour) + .show(supportFragmentManager, "TimePickerFragment") + } else { + // Launch the date picker + val dateTime = DateTime(dateInstant.date) + DatePickerFragment.getInstance( + dateTime.year, + dateTime.monthOfYear - 1, + dateTime.dayOfMonth + ) + .show(supportFragmentManager, "DatePickerFragment") + } + } - var oldNodes: List = ArrayList() - result.data?.getBundle(OLD_NODES_KEY)?.let { oldNodesBundle -> - oldNodes = getListNodesFromBundle(database, oldNodesBundle) + mGroupEditViewModel.onGroupCreated.observe(this) { groupInfo -> + if (groupInfo.title.isNotEmpty()) { + mCurrentGroup?.let { currentGroup -> + createGroup(currentGroup, groupInfo) } - var newNodes: List = ArrayList() - result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle -> - newNodes = getListNodesFromBundle(database, newNodesBundle) + } + } + + mGroupEditViewModel.onGroupUpdated.observe(this) { groupInfo -> + if (groupInfo.title.isNotEmpty()) { + mOldGroupToUpdate?.let { oldGroupToUpdate -> + updateGroup(oldGroupToUpdate, groupInfo) } + } + } + } - refreshSearchGroup() + override fun viewToInvalidateTimeout(): View? { + return rootContainerView + } - when (actionTask) { - ACTION_DATABASE_UPDATE_ENTRY_TASK -> { - if (result.isSuccess) { - mListNodesFragment?.updateNodes(oldNodes, newNodes) - EntrySelectionHelper.doSpecialAction(intent, - { - // Standard not used after task - }, - { - // Search not used - }, - { - // Save not used - }, - { - try { - val entry = newNodes[0] as Entry - entrySelectedForKeyboardSelection(entry) - } catch (e: Exception) { - Log.e(TAG, "Unable to perform action for keyboard selection after entry update", e) - } - }, - { _, _ -> - try { - val entry = newNodes[0] as Entry - entrySelectedForAutofillSelection(entry) - } catch (e: Exception) { - Log.e(TAG, "Unable to perform action for autofill selection after entry update", e) - } - }, - { - // Not use - } - ) - } - } - ACTION_DATABASE_UPDATE_GROUP_TASK -> { - if (result.isSuccess) { - mListNodesFragment?.updateNodes(oldNodes, newNodes) - } - } - ACTION_DATABASE_CREATE_GROUP_TASK, - ACTION_DATABASE_COPY_NODES_TASK, - ACTION_DATABASE_MOVE_NODES_TASK -> { - if (result.isSuccess) { - mListNodesFragment?.addNodes(newNodes) - } - } - ACTION_DATABASE_DELETE_NODES_TASK -> { - if (result.isSuccess) { + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) - // Rebuild all the list to avoid bug when delete node from sort - try { - mListNodesFragment?.rebuildList() - } catch (e: Exception) { - Log.e(TAG, "Unable to rebuild the list after deletion") - e.printStackTrace() - } + mGroupEditViewModel.setGroupNamesNotAllowed(database?.groupNamesNotAllowed) - // Add trash in views list if it doesn't exists - if (database.isRecycleBinEnabled) { - val recycleBin = database.recycleBin - val currentGroup = mCurrentGroup - if (currentGroup != null && recycleBin != null - && currentGroup != recycleBin) { - // Recycle bin already here, simply update it - if (mListNodesFragment?.contains(recycleBin) == true) { - mListNodesFragment?.updateNode(recycleBin) - } - // Recycle bin not here, verify if parents are similar to add it - else if (currentGroup == recycleBin.parent) { - mListNodesFragment?.addNode(recycleBin) - } - } - } - } - } - ACTION_DATABASE_RELOAD_TASK -> { - // Reload the current activity - if (result.isSuccess) { - reload() - } else { - this.showActionErrorIfNeeded(result) - finish() + mRecyclingBinEnabled = !mDatabaseReadOnly + && database?.isRecycleBinEnabled == true + + mRootGroup = database?.rootGroup + if (mCurrentGroupState == null) { + mRootGroup?.let { rootGroup -> + mGroupViewModel.loadGroup(database, rootGroup, 0) + } + } else { + mGroupViewModel.loadGroup(database, mCurrentGroupState) + } + + // Search suggestion + database?.let { + mSearchSuggestionAdapter = SearchEntryCursorAdapter(this, it) + mOnSuggestionListener = object : SearchView.OnSuggestionListener { + override fun onSuggestionClick(position: Int): Boolean { + mSearchSuggestionAdapter?.let { searchAdapter -> + searchAdapter.getEntryFromPosition(position)?.let { entry -> + onNodeClick(database, entry) } } + return true } - coordinatorLayout?.showActionErrorIfNeeded(result) - - finishNodeAction() - - refreshNumberOfChildren() + override fun onSuggestionSelect(position: Int): Boolean { + return true + } } } - Log.i(TAG, "Finished creating tree") + invalidateOptionsMenu() } - private fun reload() { - // Reload the current activity - startActivity(intent) - finish() - overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - mDatabase?.wasReloaded = false - } + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) - override fun onNewIntent(intent: Intent?) { - super.onNewIntent(intent) + var newNodes: List = ArrayList() + result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle -> + newNodes = getListNodesFromBundle(database, newNodesBundle) + } - intent?.let { intentNotNull -> - // To transform KEY_SEARCH_INFO in ACTION_SEARCH - manageSearchInfoIntent(intentNotNull) - Log.d(TAG, "setNewIntent: $intentNotNull") - setIntent(intentNotNull) - if (Intent.ACTION_SEARCH == intentNotNull.action) { - finishNodeAction() - // only one instance of search in backstack - deletePreviousSearchGroup() - openGroup(retrieveCurrentGroup(intentNotNull, null), true) + when (actionTask) { + ACTION_DATABASE_UPDATE_ENTRY_TASK -> { + if (result.isSuccess) { + EntrySelectionHelper.doSpecialAction(intent, + { + // Standard not used after task + }, + { + // Search not used + }, + { + // Save not used + }, + { + try { + val entry = newNodes[0] as Entry + entrySelectedForKeyboardSelection(database, entry) + } catch (e: Exception) { + Log.e( + TAG, + "Unable to perform action for keyboard selection after entry update", + e + ) + } + }, + { _, _ -> + try { + val entry = newNodes[0] as Entry + entrySelectedForAutofillSelection(database, entry) + } catch (e: Exception) { + Log.e( + TAG, + "Unable to perform action for autofill selection after entry update", + e + ) + } + }, + { + // Not use + } + ) + } } } + + coordinatorLayout?.showActionErrorIfNeeded(result) + if (!result.isSuccess) { + reloadCurrentGroup() + } + + finishNodeAction() + + refreshNumberOfChildren(mCurrentGroup) } /** - * Transform the KEY_SEARCH_INFO in ACTION_SEARCH, return true if KEY_SEARCH_INFO was present + * Transform the AUTO_SEARCH_KEY in ACTION_SEARCH, return true if AUTO_SEARCH_KEY was present */ - private fun manageSearchInfoIntent(intent: Intent): Boolean { + private fun transformSearchInfoIntent(intent: Intent) { // To relaunch the activity as ACTION_SEARCH val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveSearchInfoFromIntent(intent) val autoSearch = intent.getBooleanExtra(AUTO_SEARCH_KEY, false) + intent.removeExtra(AUTO_SEARCH_KEY) if (searchInfo != null && autoSearch) { intent.action = Intent.ACTION_SEARCH intent.putExtra(SearchManager.QUERY, searchInfo.toString()) - return true } - return false } - private fun deletePreviousSearchGroup() { - // Delete the previous search fragment - try { - val searchFragment = supportFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) - if (searchFragment != null) { - if (supportFragmentManager - .popBackStackImmediate(SEARCH_FRAGMENT_TAG, - FragmentManager.POP_BACK_STACK_INCLUSIVE)) - supportFragmentManager.beginTransaction().remove(searchFragment).commit() + private fun manageIntent(intent: Intent?) { + intent?.let { + if (intent.extras?.containsKey(GROUP_STATE_KEY) == true) { + mCurrentGroupState = intent.getParcelableExtra(GROUP_STATE_KEY) + intent.removeExtra(GROUP_STATE_KEY) + } + // To transform KEY_SEARCH_INFO in ACTION_SEARCH + transformSearchInfoIntent(intent) + if (Intent.ACTION_SEARCH == intent.action) { + finishNodeAction() + val searchString = + intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" + mGroupViewModel.loadGroupFromSearch( + mDatabase, + searchString, + PreferencesUtil.omitBackup(this) + ) } - } catch (exception: Exception) { - Log.e(TAG, "unable to remove previous search fragment", exception) } } - private fun openGroup(group: Group?, isASearch: Boolean) { - // Check TimeoutHelper - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { - // Open a group in a new fragment - val newListNodeFragment = ListNodesFragment.newInstance(group, mReadOnly, isASearch) - val fragmentTransaction = supportFragmentManager.beginTransaction() - // Different animation - val fragmentTag: String - fragmentTag = if (isASearch) { - fragmentTransaction.setCustomAnimations(R.anim.slide_in_top, R.anim.slide_out_bottom, - R.anim.slide_in_bottom, R.anim.slide_out_top) - SEARCH_FRAGMENT_TAG - } else { - fragmentTransaction.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, - R.anim.slide_in_left, R.anim.slide_out_right) - LIST_NODES_FRAGMENT_TAG - } - - fragmentTransaction.replace(R.id.nodes_list_fragment_container, - newListNodeFragment, - fragmentTag) - fragmentTransaction.addToBackStack(fragmentTag) - fragmentTransaction.commit() - - if (mSpecialMode != SpecialMode.DEFAULT) - mSelectionModeCountBackStack++ - - // Update last access time. - group?.touch(modified = false, touchParents = false) - - mListNodesFragment = newListNodeFragment - mCurrentGroup = group - assignGroupViewElements() - } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + Log.d(TAG, "setNewIntent: $intent") + setIntent(intent) + manageIntent(intent) } override fun onSaveInstanceState(outState: Bundle) { - mCurrentGroup?.let { - outState.putParcelable(GROUP_ID_KEY, it.nodeId) - } + outState.putParcelableArray(PREVIOUS_GROUPS_IDS_KEY, mPreviousGroupsIds.toTypedArray()) mOldGroupToUpdate?.let { outState.putParcelable(OLD_GROUP_TO_UPDATE_KEY, it) } @@ -467,62 +476,29 @@ class GroupActivity : LockingActivity(), super.onSaveInstanceState(outState) } - private fun refreshSearchGroup() { - deletePreviousSearchGroup() - if (mCurrentGroup?.isVirtual == true) - openGroup(retrieveCurrentGroup(intent, null), true) - } - - private fun retrieveCurrentGroup(intent: Intent, savedInstanceState: Bundle?): Group? { - // Force read only if the database is like that - mReadOnly = mDatabase?.isReadOnly == true || mReadOnly - - // If it's a search - if (Intent.ACTION_SEARCH == intent.action) { - val searchString = intent.getStringExtra(SearchManager.QUERY)?.trim { it <= ' ' } ?: "" - return mDatabase?.createVirtualGroupFromSearch(searchString, - PreferencesUtil.omitBackup(this)) - } - // else a real group - else { - var pwGroupId: NodeId<*>? = null - if (savedInstanceState != null && savedInstanceState.containsKey(GROUP_ID_KEY)) { - pwGroupId = savedInstanceState.getParcelable(GROUP_ID_KEY) - } else { - if (getIntent() != null) - pwGroupId = intent.getParcelableExtra(GROUP_ID_KEY) - } - - Log.w(TAG, "Creating tree view") - val currentGroup: Group? - currentGroup = if (pwGroupId == null) { - mRootGroup - } else { - mDatabase?.getGroupById(pwGroupId) - } - - return currentGroup - } - } - - private fun assignGroupViewElements() { + private fun assignGroupViewElements(group: Group?) { // Assign title - if (mCurrentGroup != null) { - val title = mCurrentGroup?.title - if (title != null && title.isNotEmpty()) { - if (groupNameView != null) { - groupNameView?.text = title - groupNameView?.invalidate() - } - } else { - if (groupNameView != null) { - groupNameView?.text = getText(R.string.root) - groupNameView?.invalidate() + if (group != null) { + if (groupNameView != null) { + val title = group.title + groupNameView?.text = if (title.isNotEmpty()) title else getText(R.string.root) + groupNameView?.invalidate() + } + if (groupMetaView != null) { + val meta = group.nodeId.toString() + groupMetaView?.text = meta + if (meta.isNotEmpty() + && !group.isVirtual + && PreferencesUtil.showUUID(this)) { + groupMetaView?.visibility = View.VISIBLE + } else { + groupMetaView?.visibility = View.GONE } + groupMetaView?.invalidate() } } - if (mCurrentGroup?.isVirtual == true) { + if (group?.isVirtual == true) { searchTitleView?.visibility = View.VISIBLE if (toolbar != null) { toolbar?.navigationIcon = null @@ -532,13 +508,17 @@ class GroupActivity : LockingActivity(), searchTitleView?.visibility = View.GONE // Assign the group icon depending of IconPack or custom icon iconView?.visibility = View.VISIBLE - mCurrentGroup?.let { currentGroup -> + group?.let { currentGroup -> iconView?.let { imageView -> - mDatabase?.iconDrawableFactory?.assignDatabaseIcon(imageView, currentGroup.icon, mIconColor) + mIconDrawableFactory?.assignDatabaseIcon( + imageView, + currentGroup.icon, + mIconColor + ) } if (toolbar != null) { - if (mCurrentGroup?.containsParent() == true) + if (group.containsParent()) toolbar?.setNavigationIcon(R.drawable.ic_arrow_up_white_24dp) else { toolbar?.navigationIcon = null @@ -548,35 +528,36 @@ class GroupActivity : LockingActivity(), } // Assign number of children - refreshNumberOfChildren() + refreshNumberOfChildren(group) // Hide button - initAddButton() + initAddButton(group) } - private fun initAddButton() { + private fun initAddButton(group: Group?) { addNodeButtonView?.apply { closeButtonIfOpen() // To enable add button - val addGroupEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true - var addEntryEnabled = !mReadOnly && mCurrentGroup?.isVirtual != true - mCurrentGroup?.let { + val addGroupEnabled = !mDatabaseReadOnly && group?.isVirtual != true + var addEntryEnabled = !mDatabaseReadOnly && group?.isVirtual != true + group?.let { if (!it.allowAddEntryIfIsRoot) addEntryEnabled = it != mRootGroup && addEntryEnabled } enableAddGroup(addGroupEnabled) enableAddEntry(addEntryEnabled) - if (mCurrentGroup?.isVirtual == true) + if (group?.isVirtual == true) hideButton() else if (actionNodeMode == null) showButton() } } - private fun refreshNumberOfChildren() { + private fun refreshNumberOfChildren(group: Group?) { numberChildrenView?.apply { if (PreferencesUtil.showNumberEntries(context)) { - text = mCurrentGroup?.getNumberOfChildEntries(Group.ChildFilter.getDefaults(context))?.toString() ?: "" + group?.refreshNumberOfChildEntries(Group.ChildFilter.getDefaults(context)) + text = group?.numberOfChildEntries?.toString() ?: "" visibility = View.VISIBLE } else { visibility = View.GONE @@ -589,11 +570,22 @@ class GroupActivity : LockingActivity(), addNodeButtonView?.hideOrShowButtonOnScrollListener(dy) } - override fun onNodeClick(node: Node) { + override fun onNodeClick( + database: Database, + node: Node + ) { when (node.type) { Type.GROUP -> try { + val group = node as Group + // Save the last not virtual group and it's position + if (mCurrentGroup?.isVirtual == false) { + mCurrentGroupState?.let { + mPreviousGroupsIds.add(it) + } + } // Open child group - openGroup(node as Group, false) + mGroupViewModel.loadGroup(database, group, 0) + } catch (e: ClassCastException) { Log.e(TAG, "Node can't be cast in Group") } @@ -601,170 +593,165 @@ class GroupActivity : LockingActivity(), Type.ENTRY -> try { val entryVersioned = node as Entry EntrySelectionHelper.doSpecialAction(intent, - { - EntryActivity.launch(this@GroupActivity, entryVersioned, mReadOnly) - }, - { - // Nothing here, a search is simply performed - }, - { searchInfo -> - if (!mReadOnly) - entrySelectedForSave(entryVersioned, searchInfo) - else - finish() - }, - { searchInfo -> - // Recheck search, only to fix #783 because workflow allows to open multiple search elements - SearchHelper.checkAutoSearchInfo(this, - mDatabase!!, - searchInfo, - { _ -> - // Item in search, don't save - entrySelectedForKeyboardSelection(entryVersioned) - }, - { - // Item not found, save it if required - if (!mReadOnly - && searchInfo != null - && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)) { - updateEntryWithSearchInfo(entryVersioned, searchInfo) - } else { - entrySelectedForKeyboardSelection(entryVersioned) - } - }, - { - // Normally not append - finish() - } - ) - }, - { searchInfo, _ -> - if (!mReadOnly + { + EntryActivity.launch( + this@GroupActivity, + database, + entryVersioned.nodeId + ) + }, + { + // Nothing here, a search is simply performed + }, + { searchInfo -> + if (!database.isReadOnly) + entrySelectedForSave(database, entryVersioned, searchInfo) + else + finish() + }, + { searchInfo -> + // Recheck search, only to fix #783 because workflow allows to open multiple search elements + SearchHelper.checkAutoSearchInfo(this, + database, + searchInfo, + { openedDatabase, _ -> + // Item in search, don't save + entrySelectedForKeyboardSelection(openedDatabase, entryVersioned) + }, + { + // Item not found, save it if required + if (!database.isReadOnly && searchInfo != null - && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)) { - updateEntryWithSearchInfo(entryVersioned, searchInfo) - } else { - entrySelectedForAutofillSelection(entryVersioned) - } - }, - { registerInfo -> - if (!mReadOnly) - entrySelectedForRegistration(entryVersioned, registerInfo) - else + && PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity) + ) { + updateEntryWithSearchInfo(database, entryVersioned, searchInfo) + } + entrySelectedForKeyboardSelection(database, entryVersioned) + }, + { + // Normally not append finish() - }) + } + ) + }, + { searchInfo, _ -> + if (!database.isReadOnly + && searchInfo != null + && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity) + ) { + updateEntryWithSearchInfo(database, entryVersioned, searchInfo) + } + entrySelectedForAutofillSelection(database, entryVersioned) + }, + { registerInfo -> + if (!database.isReadOnly) + entrySelectedForRegistration(database, entryVersioned, registerInfo) + else + finish() + }) } catch (e: ClassCastException) { Log.e(TAG, "Node can't be cast in Entry") } } } - private fun entrySelectedForSave(entry: Entry, searchInfo: SearchInfo) { - rebuildListNodes() + private fun entrySelectedForSave(database: Database, entry: Entry, searchInfo: SearchInfo) { + reloadCurrentGroup() // Save to update the entry - EntryEditActivity.launchForSave(this@GroupActivity, - entry, searchInfo) + EntryEditActivity.launchToUpdateForSave( + this@GroupActivity, + database, + entry.nodeId, + searchInfo + ) onLaunchActivitySpecialMode() } - private fun entrySelectedForKeyboardSelection(entry: Entry) { - rebuildListNodes() + private fun entrySelectedForKeyboardSelection(database: Database, entry: Entry) { + reloadCurrentGroup() // Populate Magikeyboard with entry - mDatabase?.let { database -> - populateKeyboardAndMoveAppToBackground(this, - entry.getEntryInfo(database), - intent) - } + populateKeyboardAndMoveAppToBackground( + this, + entry.getEntryInfo(database), + intent + ) onValidateSpecialMode() } - private fun entrySelectedForAutofillSelection(entry: Entry) { + private fun entrySelectedForAutofillSelection(database: Database, entry: Entry) { // Build response with the entry selected - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mDatabase != null) { - mDatabase?.let { database -> - AutofillHelper.buildResponseAndSetResult(this, - entry.getEntryInfo(database)) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AutofillHelper.buildResponseAndSetResult( + this, + database, + entry.getEntryInfo(database) + ) } onValidateSpecialMode() } - private fun entrySelectedForRegistration(entry: Entry, registerInfo: RegisterInfo?) { - rebuildListNodes() + private fun entrySelectedForRegistration( + database: Database, + entry: Entry, + registerInfo: RegisterInfo? + ) { + reloadCurrentGroup() // Registration to update the entry - EntryEditActivity.launchForRegistration(this@GroupActivity, - entry, registerInfo) + EntryEditActivity.launchToUpdateForRegistration( + this@GroupActivity, + database, + entry.nodeId, + registerInfo + ) onLaunchActivitySpecialMode() } - private fun updateEntryWithSearchInfo(entry: Entry, searchInfo: SearchInfo) { + private fun updateEntryWithSearchInfo( + database: Database, + entry: Entry, + searchInfo: SearchInfo + ) { val newEntry = Entry(entry) - newEntry.setEntryInfo(mDatabase, newEntry.getEntryInfo(mDatabase, true).apply { - saveSearchInfo(mDatabase, searchInfo) + newEntry.setEntryInfo(database, newEntry.getEntryInfo( + database, + raw = true, + removeTemplateConfiguration = false + ).apply { + saveSearchInfo(database, searchInfo) }) - // In selection mode, it's forced read-only, so update not allowed - mProgressDatabaseTaskProvider?.startDatabaseUpdateEntry( - entry, - newEntry, - !mReadOnly && mAutoSaveEnable - ) + updateEntry(entry, newEntry) } 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) { - val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment - groupEditFragment?.getExpiryTime()?.date?.let { expiresDate -> - groupEditFragment.setExpiryTime(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") - } + mGroupEditViewModel.selectDate(year, month, day) } } override fun onTimeSet(view: TimePicker?, hours: Int, minutes: Int) { - val groupEditFragment = supportFragmentManager.findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as? GroupEditDialogFragment - groupEditFragment?.getExpiryTime()?.date?.let { expiresDate -> - // Save the date - groupEditFragment.setExpiryTime( - DateInstant(DateTime(expiresDate) - .withHourOfDay(hours) - .withMinuteOfHour(minutes) - .toDate())) - } + mGroupEditViewModel.selectTime(hours, minutes) } private fun finishNodeAction() { actionNodeMode?.finish() } - override fun onNodeSelected(nodes: List): Boolean { + override fun onNodeSelected( + database: Database, + nodes: List + ): Boolean { if (nodes.isNotEmpty()) { if (actionNodeMode == null || toolbarAction?.getSupportActionModeCallback() == null) { - mListNodesFragment?.actionNodesCallback(nodes, this, object: ActionMode.Callback { - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return true - } - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return true - } - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - return false - } - override fun onDestroyActionMode(mode: ActionMode?) { - actionNodeMode = null - addNodeButtonView?.showButton() - } - })?.let { + mGroupFragment?.actionNodesCallback( + database, + nodes, + this + ) { _ -> + actionNodeMode = null + addNodeButtonView?.showButton() + }?.let { actionNodeMode = toolbarAction?.startSupportActionMode(it) } } else { @@ -777,139 +764,101 @@ class GroupActivity : LockingActivity(), return true } - override fun onOpenMenuClick(node: Node): Boolean { + override fun onOpenMenuClick( + database: Database, + node: Node + ): Boolean { finishNodeAction() - onNodeClick(node) + onNodeClick(database, node) return true } - override fun onEditMenuClick(node: Node): Boolean { + override fun onEditMenuClick( + database: Database, + node: Node + ): Boolean { finishNodeAction() when (node.type) { Type.GROUP -> { mOldGroupToUpdate = node as Group GroupEditDialogFragment.update(mOldGroupToUpdate!!.getGroupInfo()) - .show(supportFragmentManager, - GroupEditDialogFragment.TAG_CREATE_GROUP) + .show( + supportFragmentManager, + GroupEditDialogFragment.TAG_CREATE_GROUP + ) } - Type.ENTRY -> EntryEditActivity.launch(this@GroupActivity, node as Entry) + Type.ENTRY -> EntryEditActivity.launchToUpdate( + this@GroupActivity, + database, + (node as Entry).nodeId + ) } return true } - override fun onCopyMenuClick(nodes: List): Boolean { + override fun onCopyMenuClick( + database: Database, + nodes: List + ): Boolean { actionNodeMode?.invalidate() // Nothing here fragment calls onPasteMenuClick internally return true } - override fun onMoveMenuClick(nodes: List): Boolean { + override fun onMoveMenuClick( + database: Database, + nodes: List + ): Boolean { actionNodeMode?.invalidate() // Nothing here fragment calls onPasteMenuClick internally return true } - override fun onPasteMenuClick(pasteMode: ListNodesFragment.PasteMode?, - nodes: List): Boolean { + override fun onPasteMenuClick( + database: Database, + pasteMode: GroupFragment.PasteMode?, + nodes: List + ): Boolean { when (pasteMode) { - ListNodesFragment.PasteMode.PASTE_FROM_COPY -> { + GroupFragment.PasteMode.PASTE_FROM_COPY -> { // Copy mCurrentGroup?.let { newParent -> - mProgressDatabaseTaskProvider?.startDatabaseCopyNodes( - nodes, - newParent, - !mReadOnly && mAutoSaveEnable - ) + copyNodes(nodes, newParent) } } - ListNodesFragment.PasteMode.PASTE_FROM_MOVE -> { + GroupFragment.PasteMode.PASTE_FROM_MOVE -> { // Move mCurrentGroup?.let { newParent -> - mProgressDatabaseTaskProvider?.startDatabaseMoveNodes( - nodes, - newParent, - !mReadOnly && mAutoSaveEnable - ) + moveNodes(nodes, newParent) } } - else -> {} + else -> { + } } finishNodeAction() return true } - private fun eachNodeRecyclable(nodes: List): Boolean { - mDatabase?.let { database -> - return nodes.find { node -> - var cannotRecycle = true - if (node is Entry) { - cannotRecycle = !database.canRecycle(node) - } else if (node is Group) { - cannotRecycle = !database.canRecycle(node) - } - cannotRecycle - } == null - } - return false - } - - private fun deleteNodes(nodes: List, recycleBin: Boolean = false): Boolean { - mDatabase?.let { database -> - - // If recycle bin enabled, ensure it exists - if (database.isRecycleBinEnabled) { - database.ensureRecycleBinExists(resources) - } - - // If recycle bin enabled and not in recycle bin, move in recycle bin - if (eachNodeRecyclable(nodes)) { - mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes( - nodes, - !mReadOnly && mAutoSaveEnable - ) - } - // else open the dialog to confirm deletion - else { - val deleteNodesDialogFragment: DeleteNodesDialogFragment = - if (recycleBin) { - EmptyRecycleBinDialogFragment.getInstance(nodes) - } else { - DeleteNodesDialogFragment.getInstance(nodes) - } - deleteNodesDialogFragment.show(supportFragmentManager, "deleteNodesDialogFragment") - } - finishNodeAction() - } + override fun onDeleteMenuClick( + database: Database, + nodes: List + ): Boolean { + deleteNodes(nodes) + finishNodeAction() return true } - override fun onDeleteMenuClick(nodes: List): Boolean { - return deleteNodes(nodes) - } - - override fun permanentlyDeleteNodes(nodes: List) { - mProgressDatabaseTaskProvider?.startDatabaseDeleteNodes( - nodes, - !mReadOnly && mAutoSaveEnable - ) - } - override fun onResume() { super.onResume() - if (mDatabase?.wasReloaded == true) { - reload() - } // Show the lock button lockView?.visibility = if (PreferencesUtil.showLockDatabaseButton(this)) { View.VISIBLE } else { View.GONE } - // Refresh the elements - assignGroupViewElements() // Refresh suggestions to change preferences mSearchSuggestionAdapter?.reInit(this) // Padding if lock button visible @@ -927,7 +876,7 @@ class GroupActivity : LockingActivity(), val inflater = menuInflater inflater.inflate(R.menu.search, menu) inflater.inflate(R.menu.database, menu) - if (mReadOnly) { + if (mDatabaseReadOnly) { menu.findItem(R.id.menu_save_database)?.isVisible = false } if (mSpecialMode == SpecialMode.DEFAULT) { @@ -937,9 +886,7 @@ class GroupActivity : LockingActivity(), } // Menu for recycle bin - if (!mReadOnly - && mDatabase?.isRecycleBinEnabled == true - && mDatabase?.recycleBin == mCurrentGroup) { + if (mRecyclingBinEnabled && mRecyclingBinIsCurrentGroup) { inflater.inflate(R.menu.recycle_bin, menu) } @@ -950,29 +897,18 @@ class GroupActivity : LockingActivity(), val searchView = it.actionView as SearchView? searchView?.apply { (searchManager?.getSearchableInfo( - ComponentName(this@GroupActivity, GroupActivity::class.java)))?.let { searchableInfo -> + ComponentName(this@GroupActivity, GroupActivity::class.java) + ))?.let { searchableInfo -> setSearchableInfo(searchableInfo) } setIconifiedByDefault(false) // Do not iconify the widget; expand it by default suggestionsAdapter = mSearchSuggestionAdapter - setOnSuggestionListener(object : SearchView.OnSuggestionListener { - override fun onSuggestionClick(position: Int): Boolean { - mSearchSuggestionAdapter?.let { searchAdapter -> - searchAdapter.getEntryFromPosition(position)?.let { entry -> - onNodeClick(entry) - } - } - return true - } - - override fun onSuggestionSelect(position: Int): Boolean { - return true - } - }) + setOnSuggestionListener(mOnSuggestionListener) } // Expand the search view if defined in settings if (mRequestStartupSearch - && PreferencesUtil.automaticallyFocusSearch(this@GroupActivity)) { + && PreferencesUtil.automaticallyFocusSearch(this@GroupActivity) + ) { // To request search only one time mRequestStartupSearch = false it.expandActionView() @@ -982,65 +918,72 @@ class GroupActivity : LockingActivity(), super.onCreateOptionsMenu(menu) // Launch education screen - Handler(Looper.getMainLooper()).post { performedNextEducation(GroupActivityEducation(this), menu) } + Handler(Looper.getMainLooper()).post { + performedNextEducation( + GroupActivityEducation(this), + menu + ) + } return true } - private fun performedNextEducation(groupActivityEducation: GroupActivityEducation, - menu: Menu) { + private fun performedNextEducation( + groupActivityEducation: GroupActivityEducation, + menu: Menu + ) { // If no node, show education to add new one - val addNodeButtonEducationPerformed = mListNodesFragment != null - && mListNodesFragment!!.isEmpty + val addNodeButtonEducationPerformed = actionNodeMode == null && addNodeButtonView?.addButtonView != null && addNodeButtonView!!.isEnable && groupActivityEducation.checkAndPerformedAddNodeButtonEducation( - addNodeButtonView?.addButtonView!!, - { - addNodeButtonView?.openButtonIfClose() - }, - { - performedNextEducation(groupActivityEducation, menu) - } - ) + addNodeButtonView?.addButtonView!!, + { + addNodeButtonView?.openButtonIfClose() + }, + { + performedNextEducation(groupActivityEducation, menu) + } + ) if (!addNodeButtonEducationPerformed) { val searchMenuEducationPerformed = toolbar != null && toolbar!!.findViewById(R.id.menu_search) != null && groupActivityEducation.checkAndPerformedSearchMenuEducation( - toolbar!!.findViewById(R.id.menu_search), - { - menu.findItem(R.id.menu_search).expandActionView() - }, - { - performedNextEducation(groupActivityEducation, menu) - }) + toolbar!!.findViewById(R.id.menu_search), + { + menu.findItem(R.id.menu_search).expandActionView() + }, + { + performedNextEducation(groupActivityEducation, menu) + }) if (!searchMenuEducationPerformed) { val sortMenuEducationPerformed = toolbar != null && toolbar!!.findViewById(R.id.menu_sort) != null && groupActivityEducation.checkAndPerformedSortMenuEducation( - toolbar!!.findViewById(R.id.menu_sort), - { - onOptionsItemSelected(menu.findItem(R.id.menu_sort)) - }, - { - performedNextEducation(groupActivityEducation, menu) - }) + toolbar!!.findViewById(R.id.menu_sort), + { + onOptionsItemSelected(menu.findItem(R.id.menu_sort)) + }, + { + performedNextEducation(groupActivityEducation, menu) + }) if (!sortMenuEducationPerformed) { // lockMenuEducationPerformed val lockButtonView = findViewById(R.id.lock_button_icon) lockButtonView != null - && groupActivityEducation.checkAndPerformedLockMenuEducation(lockButtonView, - { - lockAndExit() - }, - { - performedNextEducation(groupActivityEducation, menu) - }) + && groupActivityEducation.checkAndPerformedLockMenuEducation( + lockButtonView, + { + lockAndExit() + }, + { + performedNextEducation(groupActivityEducation, menu) + }) } } } @@ -1056,91 +999,36 @@ class GroupActivity : LockingActivity(), //onSearchRequested(); return true R.id.menu_save_database -> { - mProgressDatabaseTaskProvider?.startDatabaseSave(!mReadOnly) + saveDatabase() return true } R.id.menu_reload_database -> { - mProgressDatabaseTaskProvider?.startDatabaseReload(false) + reloadDatabase() return true } R.id.menu_empty_recycle_bin -> { - mCurrentGroup?.getChildren()?.let { listChildren -> - // Automatically delete all elements - deleteNodes(listChildren, true) + if (mRecyclingBinEnabled && mRecyclingBinIsCurrentGroup) { + mCurrentGroup?.getChildren()?.let { listChildren -> + // Automatically delete all elements + deleteNodes(listChildren, true) + finishNodeAction() + } } return true } else -> { // Check the time lock before launching settings - MenuUtil.onDefaultMenuOptionsItemSelected(this, item, mReadOnly, true) + MenuUtil.onDefaultMenuOptionsItemSelected(this, item, true) return super.onOptionsItemSelected(item) } } } - override fun isValidGroupName(name: String): GroupEditDialogFragment.Error { - if (name.isEmpty()) { - return GroupEditDialogFragment.Error(true, R.string.error_no_name) - } - if (mDatabase?.groupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null) { - return GroupEditDialogFragment.Error(true, R.string.error_word_reserved) - } - return GroupEditDialogFragment.Error(false, null) - } - - override fun approveEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, - groupInfo: GroupInfo) { - - if (groupInfo.title.isNotEmpty()) { - when (action) { - GroupEditDialogFragment.EditGroupDialogAction.CREATION -> { - // If group creation - mCurrentGroup?.let { currentGroup -> - // Build the group - mDatabase?.createGroup()?.let { newGroup -> - newGroup.setGroupInfo(groupInfo) - // Not really needed here because added in runnable but safe - newGroup.parent = currentGroup - - mProgressDatabaseTaskProvider?.startDatabaseCreateGroup( - newGroup, - currentGroup, - !mReadOnly && mAutoSaveEnable - ) - } - } - } - GroupEditDialogFragment.EditGroupDialogAction.UPDATE -> { - // If update add new elements - mOldGroupToUpdate?.let { oldGroupToUpdate -> - val updateGroup = Group(oldGroupToUpdate).let { updateGroup -> - updateGroup.apply { - // WARNING remove parent and children to keep memory - removeParent() - removeChildren() - this.setGroupInfo(groupInfo) - } - } - // If group updated save it in the database - mProgressDatabaseTaskProvider?.startDatabaseUpdateGroup( - oldGroupToUpdate, - updateGroup, - !mReadOnly && mAutoSaveEnable - ) - } - } - else -> {} - } - } - } - - override fun cancelEditGroup(action: GroupEditDialogFragment.EditGroupDialogAction, - groupInfo: GroupInfo) { - // Do nothing here - } - - override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { - mListNodesFragment?.onSortSelected(sortNodeEnum, sortNodeParameters) + override fun onSortSelected( + sortNodeEnum: SortNodeEnum, + sortNodeParameters: SortNodeEnum.SortNodeParameters + ) { + mGroupFragment?.onSortSelected(sortNodeEnum, sortNodeParameters) } override fun startActivity(intent: Intent) { @@ -1179,9 +1067,7 @@ class GroupActivity : LockingActivity(), // To create tree dialog for icon IconPickerActivity.onActivityResult(requestCode, resultCode, data) { icon -> - (supportFragmentManager - .findFragmentByTag(GroupEditDialogFragment.TAG_CREATE_GROUP) as GroupEditDialogFragment) - .setIcon(icon) + mGroupEditViewModel.selectIcon(icon) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -1189,44 +1075,51 @@ class GroupActivity : LockingActivity(), } // Directly used the onActivityResult in fragment - mListNodesFragment?.onActivityResult(requestCode, resultCode, data) + mGroupFragment?.onActivityResult(requestCode, resultCode, data) } - private fun rebuildListNodes() { - mListNodesFragment = supportFragmentManager.findFragmentByTag(LIST_NODES_FRAGMENT_TAG) as ListNodesFragment? - // to refresh fragment - try { - mListNodesFragment?.rebuildList() - } catch (e: Exception) { - e.printStackTrace() - coordinatorLayout?.let { coordinatorLayout -> - Snackbar.make(coordinatorLayout, - R.string.error_rebuild_list, - Snackbar.LENGTH_LONG).asError().show() - } - } - mCurrentGroup = mListNodesFragment?.mainGroup - // Remove search in intent - deletePreviousSearchGroup() + private fun removeSearch() { + intent.removeExtra(AUTO_SEARCH_KEY) if (Intent.ACTION_SEARCH == intent.action) { intent.action = Intent.ACTION_DEFAULT intent.removeExtra(SearchManager.QUERY) } - assignGroupViewElements() + } + + private fun reloadCurrentGroup() { + // Remove search in intent + removeSearch() + // Reload real group + try { + mGroupViewModel.loadGroup(mDatabase, mCurrentGroupState) + } catch (e: Exception) { + Log.e(TAG, "Unable to rebuild the list after deletion", e) + } } override fun onBackPressed() { - if (mListNodesFragment?.nodeActionSelectionMode == true) { + if (mGroupFragment?.nodeActionSelectionMode == true) { finishNodeAction() } else { // Normal way when we are not in root if (mRootGroup != null && mRootGroup != mCurrentGroup) { - super.onRegularBackPressed() - rebuildListNodes() + when { + Intent.ACTION_SEARCH == intent.action -> { + // Remove the search + reloadCurrentGroup() + } + mPreviousGroupsIds.isEmpty() -> { + super.onRegularBackPressed() + } + else -> { + // Load the previous group + mGroupViewModel.loadGroup(mDatabase, mPreviousGroupsIds.removeLast()) + } + } } // Else in root, lock if needed else { - intent.removeExtra(AUTO_SEARCH_KEY) + removeSearch() EntrySelectionHelper.removeModesFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent) if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { @@ -1239,25 +1132,32 @@ class GroupActivity : LockingActivity(), } } - private fun removeFragmentHistory() { - val fragmentManager = supportFragmentManager - if (mSelectionModeCountBackStack > 0) { - for (selectionMode in 0 .. mSelectionModeCountBackStack) { - fragmentManager.popBackStack() - } + data class GroupState( + var groupId: NodeId<*>?, + var firstVisibleItem: Int? + ) : Parcelable { + + private constructor(parcel: Parcel) : this( + parcel.readParcelable>(NodeId::class.java.classLoader), + parcel.readInt() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(groupId, flags) + parcel.writeInt(firstVisibleItem ?: 0) } - // Reinit the counter for navigation history - mSelectionModeCountBackStack = 0 - } - override fun onValidateSpecialMode() { - removeFragmentHistory() - super.onValidateSpecialMode() - } + override fun describeContents() = 0 - override fun onCancelSpecialMode() { - removeFragmentHistory() - super.onCancelSpecialMode() + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): GroupState { + return GroupState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } } companion object { @@ -1265,39 +1165,35 @@ class GroupActivity : LockingActivity(), private val TAG = GroupActivity::class.java.name private const val REQUEST_STARTUP_SEARCH_KEY = "REQUEST_STARTUP_SEARCH_KEY" - private const val GROUP_ID_KEY = "GROUP_ID_KEY" - private const val LIST_NODES_FRAGMENT_TAG = "LIST_NODES_FRAGMENT_TAG" - private const val SEARCH_FRAGMENT_TAG = "SEARCH_FRAGMENT_TAG" + private const val GROUP_STATE_KEY = "GROUP_STATE_KEY" + private const val PREVIOUS_GROUPS_IDS_KEY = "PREVIOUS_GROUPS_IDS_KEY" + private const val GROUP_FRAGMENT_TAG = "GROUP_FRAGMENT_TAG" private const val OLD_GROUP_TO_UPDATE_KEY = "OLD_GROUP_TO_UPDATE_KEY" private const val AUTO_SEARCH_KEY = "AUTO_SEARCH_KEY" private fun buildIntent(context: Context, - group: Group?, - readOnly: Boolean, + groupState: GroupState?, intentBuildLauncher: (Intent) -> Unit) { val intent = Intent(context, GroupActivity::class.java) - if (group != null) { - intent.putExtra(GROUP_ID_KEY, group.nodeId) + if (groupState != null) { + intent.putExtra(GROUP_STATE_KEY, groupState) } - ReadOnlyHelper.putReadOnlyInIntent(intent, readOnly) intentBuildLauncher.invoke(intent) } private fun checkTimeAndBuildIntent(activity: Activity, - group: Group?, - readOnly: Boolean, + groupState: GroupState?, intentBuildLauncher: (Intent) -> Unit) { if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) { - buildIntent(activity, group, readOnly, intentBuildLauncher) + buildIntent(activity, groupState, intentBuildLauncher) } } private fun checkTimeAndBuildIntent(context: Context, - group: Group?, - readOnly: Boolean, + groupState: GroupState?, intentBuildLauncher: (Intent) -> Unit) { if (TimeoutHelper.checkTime(context)) { - buildIntent(context, group, readOnly, intentBuildLauncher) + buildIntent(context, groupState, intentBuildLauncher) } } @@ -1307,11 +1203,13 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launch(context: Context, - readOnly: Boolean, + database: Database, autoSearch: Boolean = false) { - checkTimeAndBuildIntent(context, null, readOnly) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - context.startActivity(intent) + if (database.loaded) { + checkTimeAndBuildIntent(context, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, autoSearch) + context.startActivity(intent) + } } } @@ -1321,15 +1219,18 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launchForSearchResult(context: Context, - readOnly: Boolean, + database: Database, searchInfo: SearchInfo, autoSearch: Boolean = false) { - checkTimeAndBuildIntent(context, null, readOnly) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - EntrySelectionHelper.addSearchInfoInIntent( + if (database.loaded) { + checkTimeAndBuildIntent(context, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, autoSearch) + EntrySelectionHelper.addSearchInfoInIntent( intent, - searchInfo) - context.startActivity(intent) + searchInfo + ) + context.startActivity(intent) + } } } @@ -1339,13 +1240,18 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launchForSaveResult(context: Context, + database: Database, searchInfo: SearchInfo, autoSearch: Boolean = false) { - checkTimeAndBuildIntent(context, null, false) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - EntrySelectionHelper.startActivityForSaveModeResult(context, + if (database.loaded && !database.isReadOnly) { + checkTimeAndBuildIntent(context, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, autoSearch) + EntrySelectionHelper.startActivityForSaveModeResult( + context, intent, - searchInfo) + searchInfo + ) + } } } @@ -1355,14 +1261,18 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launchForKeyboardSelectionResult(context: Context, - readOnly: Boolean, + database: Database, searchInfo: SearchInfo? = null, autoSearch: Boolean = false) { - checkTimeAndBuildIntent(context, null, readOnly) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(context, + if (database.loaded) { + checkTimeAndBuildIntent(context, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, autoSearch) + EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( + context, intent, - searchInfo) + searchInfo + ) + } } } @@ -1373,16 +1283,20 @@ class GroupActivity : LockingActivity(), */ @RequiresApi(api = Build.VERSION_CODES.O) fun launchForAutofillResult(activity: Activity, - readOnly: Boolean, + database: Database, autofillComponent: AutofillComponent, searchInfo: SearchInfo? = null, autoSearch: Boolean = false) { - checkTimeAndBuildIntent(activity, null, readOnly) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, autoSearch) - AutofillHelper.startActivityForAutofillResult(activity, + if (database.loaded) { + checkTimeAndBuildIntent(activity, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, autoSearch) + AutofillHelper.startActivityForAutofillResult( + activity, intent, autofillComponent, - searchInfo) + searchInfo + ) + } } } @@ -1392,12 +1306,17 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launchForRegistration(context: Context, + database: Database, registerInfo: RegisterInfo? = null) { - checkTimeAndBuildIntent(context, null, false) { intent -> - intent.putExtra(AUTO_SEARCH_KEY, false) - EntrySelectionHelper.startActivityForRegistrationModeResult(context, + if (database.loaded && !database.isReadOnly) { + checkTimeAndBuildIntent(context, null) { intent -> + intent.putExtra(AUTO_SEARCH_KEY, false) + EntrySelectionHelper.startActivityForRegistrationModeResult( + context, intent, - registerInfo) + registerInfo + ) + } } } @@ -1407,39 +1326,42 @@ class GroupActivity : LockingActivity(), * ------------------------- */ fun launch(activity: Activity, - readOnly: Boolean, + database: Database, onValidateSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit) { EntrySelectionHelper.doSpecialAction(activity.intent, { - GroupActivity.launch(activity, - readOnly, - true) + GroupActivity.launch( + activity, + database, + true + ) }, { searchInfo -> SearchHelper.checkAutoSearchInfo(activity, - Database.getInstance(), + database, searchInfo, - { _ -> + { _, _ -> // Response is build GroupActivity.launchForSearchResult(activity, - readOnly, - searchInfo, - true) + database, + searchInfo, + true) onLaunchActivitySpecialMode() }, { // Here no search info found - if (readOnly) { + if (database.isReadOnly) { GroupActivity.launchForSearchResult(activity, - readOnly, - searchInfo, - false) + database, + searchInfo, + false) } else { GroupActivity.launchForSaveResult(activity, - searchInfo, - false) + database, + searchInfo, + false) } onLaunchActivitySpecialMode() }, @@ -1451,24 +1373,31 @@ class GroupActivity : LockingActivity(), }, { searchInfo -> // Save info used with OTP - if (!readOnly) { - GroupActivity.launchForSaveResult(activity, + if (database.loaded) { + if (!database.isReadOnly) { + GroupActivity.launchForSaveResult( + activity, + database, searchInfo, - false) - onLaunchActivitySpecialMode() - } else { - Toast.makeText(activity.applicationContext, + false + ) + onLaunchActivitySpecialMode() + } else { + Toast.makeText( + activity.applicationContext, R.string.autofill_read_only_save, - Toast.LENGTH_LONG) + Toast.LENGTH_LONG + ) .show() - onCancelSpecialMode() + onCancelSpecialMode() + } } }, { searchInfo -> SearchHelper.checkAutoSearchInfo(activity, - Database.getInstance(), + database, searchInfo, - { items -> + { _, items -> // Response is build if (items.size == 1) { populateKeyboardAndMoveAppToBackground(activity, @@ -1478,18 +1407,18 @@ class GroupActivity : LockingActivity(), } else { // Select the one we want GroupActivity.launchForKeyboardSelectionResult(activity, - readOnly, - searchInfo, - true) + database, + searchInfo, + true) onLaunchActivitySpecialMode() } }, { // Here no search info found, disable auto search GroupActivity.launchForKeyboardSelectionResult(activity, - readOnly, - searchInfo, - false) + database, + searchInfo, + false) onLaunchActivitySpecialMode() }, { @@ -1501,20 +1430,20 @@ class GroupActivity : LockingActivity(), { searchInfo, autofillComponent -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { SearchHelper.checkAutoSearchInfo(activity, - Database.getInstance(), + database, searchInfo, - { items -> + { openedDatabase, items -> // Response is build - AutofillHelper.buildResponseAndSetResult(activity, items) + AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items) onValidateSpecialMode() }, { // Here no search info found, disable auto search GroupActivity.launchForAutofillResult(activity, - readOnly, - autofillComponent, - searchInfo, - false) + database, + autofillComponent, + searchInfo, + false) onLaunchActivitySpecialMode() }, { @@ -1527,20 +1456,22 @@ class GroupActivity : LockingActivity(), } }, { registerInfo -> - if (!readOnly) { + if (!database.isReadOnly) { SearchHelper.checkAutoSearchInfo(activity, - Database.getInstance(), + database, registerInfo?.searchInfo, - { _ -> + { _, _ -> // No auto search, it's a registration GroupActivity.launchForRegistration(activity, - registerInfo) + database, + registerInfo) onLaunchActivitySpecialMode() }, { // Here no search info found, disable auto search GroupActivity.launchForRegistration(activity, - registerInfo) + database, + registerInfo) onLaunchActivitySpecialMode() }, { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt index 403647a79..93635b1a3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/IconPickerActivity.kt @@ -36,8 +36,7 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.fragments.IconPickerFragment import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener -import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImageCustom @@ -50,7 +49,7 @@ import com.kunzisoft.keepass.viewmodels.IconPickerViewModel import kotlinx.coroutines.* -class IconPickerActivity : LockingActivity() { +class IconPickerActivity : DatabaseLockActivity() { private lateinit var toolbar: Toolbar private lateinit var coordinatorLayout: CoordinatorLayout @@ -65,8 +64,6 @@ class IconPickerActivity : LockingActivity() { private var mCustomIconsSelectionMode = false private var mIconsSelected: List = ArrayList() - private var mDatabase: Database? = null - private var mExternalFileHelper: ExternalFileHelper? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -74,8 +71,6 @@ class IconPickerActivity : LockingActivity() { setContentView(R.layout.activity_icon_picker) - mDatabase = Database.getInstance() - toolbar = findViewById(R.id.toolbar) toolbar.title = " " setSupportActionBar(toolbar) @@ -88,11 +83,6 @@ class IconPickerActivity : LockingActivity() { mExternalFileHelper = ExternalFileHelper(this) uploadButton = findViewById(R.id.icon_picker_upload) - if (mDatabase?.allowCustomIcons == true) { - uploadButton.setOpenDocumentClickListener(mExternalFileHelper) - } else { - uploadButton.visibility = View.GONE - } lockView = findViewById(R.id.lock_button) lockView?.setOnClickListener { @@ -118,9 +108,6 @@ class IconPickerActivity : LockingActivity() { mIconImage = savedInstanceState.getParcelable(EXTRA_ICON) ?: mIconImage } - // Focus view to reinitialize timeout - findViewById(R.id.icon_picker_container)?.resetAppTimeoutWhenViewFocusedOrChanged(this) - iconPickerViewModel.standardIconPicked.observe(this) { iconStandard -> mIconImage.standard = iconStandard // Remove the custom icon if a standard one is selected @@ -154,6 +141,24 @@ class IconPickerActivity : LockingActivity() { } } + override fun viewToInvalidateTimeout(): View? { + return findViewById(R.id.icon_picker_container) + } + + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + + if (database?.allowCustomIcons == true) { + uploadButton.setOpenDocumentClickListener(mExternalFileHelper) + } else { + uploadButton.visibility = View.GONE + } + } + private fun updateIconsSelectedViews() { if (mIconsSelected.isEmpty()) { mCustomIconsSelectionMode = false @@ -187,11 +192,16 @@ class IconPickerActivity : LockingActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { super.onCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.icon, menu) + return true + } - if (mCustomIconsSelectionMode) { - menuInflater.inflate(R.menu.icon, menu) + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + menu?.findItem(R.id.menu_delete)?.apply { + isEnabled = mCustomIconsSelectionMode + isVisible = isEnabled } - return true + return super.onPrepareOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -208,6 +218,9 @@ class IconPickerActivity : LockingActivity() { removeCustomIcon(iconToRemove) } } + R.id.menu_external_icon -> { + UriUtil.gotoUrl(this, R.string.external_icon_url) + } } return super.onOptionsItemSelected(item) diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt index b0d27eab8..9cc56f9f5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/ImageViewerActivity.kt @@ -19,6 +19,7 @@ */ package com.kunzisoft.keepass.activities +import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle @@ -31,16 +32,19 @@ import android.widget.ImageView import androidx.appcompat.widget.Toolbar import com.igreenwood.loupe.Loupe import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.lock.LockingActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import kotlin.math.max -class ImageViewerActivity : LockingActivity() { +class ImageViewerActivity : DatabaseLockActivity() { - private var mDatabase: Database? = null + private var imageContainerView: ViewGroup? = null + private lateinit var imageView: ImageView + private lateinit var progressView: View + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,18 +54,54 @@ class ImageViewerActivity : LockingActivity() { setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + toolbar.setOnTouchListener { _, _ -> + resetAppTimeout() + false + } - val imageContainerView: ViewGroup = findViewById(R.id.image_viewer_container) - val imageView: ImageView = findViewById(R.id.image_viewer_image) - val progressView: View = findViewById(R.id.image_viewer_progress) + imageContainerView = findViewById(R.id.image_viewer_container) + imageView = findViewById(R.id.image_viewer_image) + progressView = findViewById(R.id.image_viewer_progress) - // Approximately, to not OOM and allow a zoom - val mImagePreviewMaxWidth = max( - resources.displayMetrics.widthPixels * 2, - resources.displayMetrics.heightPixels * 2 - ) + Loupe.create(imageView, imageContainerView!!) { + onViewTouchedListener = View.OnTouchListener { _, _ -> + // to reset timeout when Loupe image view touched + resetAppTimeout() + false + } + onViewTranslateListener = object : Loupe.OnViewTranslateListener { - mDatabase = Database.getInstance() + override fun onStart(view: ImageView) { + // called when the view starts moving + } + + override fun onViewTranslate(view: ImageView, amount: Float) { + // called whenever the view position changed + } + + override fun onRestore(view: ImageView) { + // called when the view drag gesture ended + } + + override fun onDismiss(view: ImageView) { + // called when the view drag gesture ended + finish() + } + } + } + } + + override fun viewToInvalidateTimeout(): View? { + // Null to manually manage events + return null + } + + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) try { progressView.visibility = View.VISIBLE @@ -72,11 +112,17 @@ class ImageViewerActivity : LockingActivity() { val size = attachment.binaryData.getSize() supportActionBar?.subtitle = Formatter.formatFileSize(this, size) - mDatabase?.let { database -> + // Approximately, to not OOM and allow a zoom + val mImagePreviewMaxWidth = max( + resources.displayMetrics.widthPixels * 2, + resources.displayMetrics.heightPixels * 2 + ) + + database?.let { database -> BinaryDatabaseManager.loadBitmap( - database, - attachment.binaryData, - mImagePreviewMaxWidth + database, + attachment.binaryData, + mImagePreviewMaxWidth ) { bitmapLoaded -> if (bitmapLoaded == null) { finish() @@ -91,28 +137,6 @@ class ImageViewerActivity : LockingActivity() { Log.e(TAG, "Unable to view the binary", e) finish() } - - Loupe.create(imageView, imageContainerView) { - onViewTranslateListener = object : Loupe.OnViewTranslateListener { - - override fun onStart(view: ImageView) { - // called when the view starts moving - } - - override fun onViewTranslate(view: ImageView, amount: Float) { - // called whenever the view position changed - } - - override fun onRestore(view: ImageView) { - // called when the view drag gesture ended - } - - override fun onDismiss(view: ImageView) { - // called when the view drag gesture ended - finish() - } - } - } } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MagikeyboardLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MagikeyboardLauncherActivity.kt index 4583265d0..44a107c5f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/MagikeyboardLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/MagikeyboardLauncherActivity.kt @@ -19,36 +19,41 @@ */ package com.kunzisoft.keepass.activities -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.search.SearchHelper /** * Activity to select entry in database and populate it in Magikeyboard */ -class MagikeyboardLauncherActivity : AppCompatActivity() { +class MagikeyboardLauncherActivity : DatabaseModeActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - val database = Database.getInstance() - val readOnly = database.isReadOnly + override fun applyCustomStyle(): Boolean { + return false + } + + override fun finishActivityIfReloadRequested(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) SearchHelper.checkAutoSearchInfo(this, - database, - null, - { - // Not called - // if items found directly returns before calling this activity - }, - { - // Select if not found - GroupActivity.launchForKeyboardSelectionResult(this, readOnly) - }, - { - // Pass extra to get entry - FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this) - } + database, + null, + { _, _ -> + // Not called + // if items found directly returns before calling this activity + }, + { openedDatabase -> + // Select if not found + GroupActivity.launchForKeyboardSelectionResult(this, openedDatabase) + }, + { + // Pass extra to get entry + FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this) + } ) finish() - super.onCreate(savedInstanceState) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt index 566238563..e2973d645 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/PasswordActivity.kt @@ -31,8 +31,11 @@ import android.text.Editable import android.text.TextWatcher import android.util.Log import android.view.* +import android.view.KeyEvent.KEYCODE_ENTER import android.view.inputmethod.EditorInfo.IME_ACTION_DONE +import android.view.inputmethod.InputMethodManager import android.widget.* +import android.widget.TextView.OnEditorActionListener import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.widget.Toolbar @@ -43,13 +46,12 @@ import com.google.android.material.snackbar.Snackbar import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog import com.kunzisoft.keepass.activities.helpers.* -import com.kunzisoft.keepass.activities.lock.LockingActivity -import com.kunzisoft.keepass.activities.selection.SpecialModeActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity +import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity import com.kunzisoft.keepass.app.database.CipherDatabaseEntity import com.kunzisoft.keepass.autofill.AutofillComponent import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment -import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException @@ -63,6 +65,7 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion. import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION import com.kunzisoft.keepass.utils.MenuUtil import com.kunzisoft.keepass.utils.UriUtil @@ -71,7 +74,8 @@ import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import java.io.FileNotFoundException -open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.BuilderListener { + +open class PasswordActivity : DatabaseModeActivity(), AdvancedUnlockFragment.BuilderListener { // Views private var toolbar: Toolbar? = null @@ -95,11 +99,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil private var mExternalFileHelper: ExternalFileHelper? = null private var mPermissionAsked = false - private var readOnly: Boolean = false + private var mReadOnly: Boolean = false private var mForceReadOnly: Boolean = false set(value) { infoContainerView?.visibility = if (value) { - readOnly = true + mReadOnly = true View.VISIBLE } else { View.GONE @@ -107,8 +111,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil field = value } - private var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null - private var mAllowAutoOpenBiometricPrompt: Boolean = true override fun onCreate(savedInstanceState: Bundle?) { @@ -132,7 +134,11 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil coordinatorLayout = findViewById(R.id.activity_password_coordinator_layout) mPermissionAsked = savedInstanceState?.getBoolean(KEY_PERMISSION_ASKED) ?: mPermissionAsked - readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrPreference(this, savedInstanceState) + mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) { + savedInstanceState.getBoolean(KEY_READ_ONLY) + } else { + PreferencesUtil.enableReadOnlyDatabase(this) + } mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mExternalFileHelper = ExternalFileHelper(this@PasswordActivity) @@ -149,6 +155,15 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil checkboxPasswordView?.isChecked = true } }) + passwordView?.setOnKeyListener { _, _, keyEvent -> + var handled = false + if (keyEvent.action == KeyEvent.ACTION_DOWN + && keyEvent?.keyCode == KEYCODE_ENTER) { + verifyCheckboxesAndLoadDatabase() + handled = true + } + handled + } // If is a view intent getUriFromIntent(intent) @@ -204,72 +219,114 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) } + } + + override fun onResume() { + super.onResume() + + mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@PasswordActivity) + + // Back to previous keyboard is setting activated + if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@PasswordActivity)) { + sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) + } + + // Don't allow auto open prompt if lock become when UI visible + mAllowAutoOpenBiometricPrompt = if (DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) + false + else + mAllowAutoOpenBiometricPrompt + mDatabaseFileUri?.let { databaseFileUri -> + databaseFileViewModel.loadDatabaseFile(databaseFileUri) + } + + checkPermission() + + mDatabase?.let { database -> + launchGroupActivityIfLoaded(database) + } + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + if (database != null) { + launchGroupActivityIfLoaded(database) + } + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + when (actionTask) { + ACTION_DATABASE_LOAD_TASK -> { + // Recheck advanced unlock if error + advancedUnlockFragment?.initAdvancedUnlockMode() + + if (result.isSuccess) { + launchGroupActivityIfLoaded(database) + } else { + passwordView?.requestFocusFromTouch() + + var resultError = "" + val resultException = result.exception + val resultMessage = result.message + + if (resultException != null) { + resultError = resultException.getLocalizedMessage(resources) - mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this).apply { - onActionFinish = { actionTask, result -> - when (actionTask) { - ACTION_DATABASE_LOAD_TASK -> { - // Recheck advanced unlock if error - advancedUnlockFragment?.initAdvancedUnlockMode() - - if (result.isSuccess) { - mDatabaseKeyFileUri = null - clearCredentialsViews(true) - launchGroupActivity() - } else { - var resultError = "" - val resultException = result.exception - val resultMessage = result.message - - if (resultException != null) { - resultError = resultException.getLocalizedMessage(resources) - - when (resultException) { - is DuplicateUuidDatabaseException -> { - // Relaunch loading if we need to fix UUID - showLoadDatabaseDuplicateUuidMessage { - - var databaseUri: Uri? = null - var mainCredential: MainCredential = MainCredential() - var readOnly = true - var cipherEntity: CipherDatabaseEntity? = null - - result.data?.let { resultData -> - databaseUri = resultData.getParcelable(DATABASE_URI_KEY) - mainCredential = resultData.getParcelable(MAIN_CREDENTIAL_KEY) ?: mainCredential - readOnly = resultData.getBoolean(READ_ONLY_KEY) - cipherEntity = resultData.getParcelable(CIPHER_ENTITY_KEY) - } - - databaseUri?.let { databaseFileUri -> - showProgressDialogAndLoadDatabase( - databaseFileUri, - mainCredential, - readOnly, - cipherEntity, - true) - } - } + when (resultException) { + is DuplicateUuidDatabaseException -> { + // Relaunch loading if we need to fix UUID + showLoadDatabaseDuplicateUuidMessage { + + var databaseUri: Uri? = null + var mainCredential = MainCredential() + var readOnly = true + var cipherEntity: CipherDatabaseEntity? = null + + result.data?.let { resultData -> + databaseUri = resultData.getParcelable(DATABASE_URI_KEY) + mainCredential = + resultData.getParcelable(MAIN_CREDENTIAL_KEY) + ?: mainCredential + readOnly = resultData.getBoolean(READ_ONLY_KEY) + cipherEntity = + resultData.getParcelable(CIPHER_ENTITY_KEY) } - is FileNotFoundDatabaseException -> { - // Remove this default database inaccessible - if (mDefaultDatabase) { - databaseFileViewModel.removeDefaultDatabase() - } + + databaseUri?.let { databaseFileUri -> + showProgressDialogAndLoadDatabase( + databaseFileUri, + mainCredential, + readOnly, + cipherEntity, + true + ) } } } - - // Show error message - if (resultMessage != null && resultMessage.isNotEmpty()) { - resultError = "$resultError $resultMessage" + is FileNotFoundDatabaseException -> { + // Remove this default database inaccessible + if (mDefaultDatabase) { + databaseFileViewModel.removeDefaultDatabase() + } } - Log.e(TAG, resultError) - Snackbar.make(coordinatorLayout, - resultError, - Snackbar.LENGTH_LONG).asError().show() } } + + // Show error message + if (resultMessage != null && resultMessage.isNotEmpty()) { + resultError = "$resultError $resultMessage" + } + Log.e(TAG, resultError) + Snackbar.make( + coordinatorLayout, + resultError, + Snackbar.LENGTH_LONG + ).asError().show() } } } @@ -296,13 +353,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil getUriFromIntent(intent) } - private fun launchGroupActivity() { - GroupActivity.launch(this, - readOnly, + private fun launchGroupActivityIfLoaded(database: Database) { + // Check if database really loaded + if (database.loaded) { + clearCredentialsViews(true) + GroupActivity.launch(this, + database, { onValidateSpecialMode() }, { onCancelSpecialMode() }, { onLaunchActivitySpecialMode() } - ) + ) + } } override fun onValidateSpecialMode() { @@ -352,40 +413,6 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil } } - override fun onResume() { - super.onResume() - - if (Database.getInstance().loaded) { - launchGroupActivity() - } else { - mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) - - // If the database isn't accessible make sure to clear the password field, if it - // was saved in the instance state - if (Database.getInstance().loaded) { - clearCredentialsViews() - } - - mProgressDatabaseTaskProvider?.registerProgressTask() - - // Back to previous keyboard is setting activated - if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this)) { - sendBroadcast(Intent(BACK_PREVIOUS_KEYBOARD_ACTION)) - } - - // Don't allow auto open prompt if lock become when UI visible - mAllowAutoOpenBiometricPrompt = if (LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == true) - false - else - mAllowAutoOpenBiometricPrompt - mDatabaseFileUri?.let { databaseFileUri -> - databaseFileViewModel.loadDatabaseFile(databaseFileUri) - } - - checkPermission() - } - } - private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { // Define Key File text if (mRememberKeyFile) { @@ -409,11 +436,17 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil } else { // Init Biometric elements advancedUnlockFragment?.loadDatabase(databaseFileUri, - mAllowAutoOpenBiometricPrompt - && mProgressDatabaseTaskProvider?.isBinded() != true) + mAllowAutoOpenBiometricPrompt) } enableOrNotTheConfirmationButton() + + // Auto select the password field and open keyboard + passwordView?.postDelayed({ + passwordView?.requestFocusFromTouch() + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager? + inputMethodManager?.showSoftInput(passwordView, InputMethodManager.SHOW_IMPLICIT) + }, 100) } private fun enableOrNotTheConfirmationButton() { @@ -431,6 +464,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { populatePasswordTextView(null) if (clearKeyFile) { + mDatabaseKeyFileUri = null populateKeyFileTextView(null) } } @@ -460,10 +494,8 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil } override fun onPause() { - mProgressDatabaseTaskProvider?.unregisterProgressTask() - // Reinit locking activity UI variable - LockingActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null + DatabaseLockActivity.LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = null mAllowAutoOpenBiometricPrompt = true super.onPause() @@ -474,7 +506,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil mDatabaseKeyFileUri?.let { outState.putString(KEY_KEYFILE, it.toString()) } - ReadOnlyHelper.onSaveInstanceState(outState, readOnly) + outState.putBoolean(KEY_READ_ONLY, mReadOnly) outState.putBoolean(ALLOW_AUTO_OPEN_BIOMETRIC_PROMPT, false) super.onSaveInstanceState(outState) } @@ -512,7 +544,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil clearCredentialsViews() } - if (readOnly && ( + if (mReadOnly && ( mSpecialMode == SpecialMode.SAVE || mSpecialMode == SpecialMode.REGISTRATION) ) { @@ -526,7 +558,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil showProgressDialogAndLoadDatabase( databaseUri, MainCredential(password, keyFileUri), - readOnly, + mReadOnly, cipherDatabaseEntity, false) } @@ -538,7 +570,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil readOnly: Boolean, cipherDatabaseEntity: CipherDatabaseEntity?, fixDuplicateUUID: Boolean) { - mProgressDatabaseTaskProvider?.startDatabaseLoad( + loadDatabase( databaseUri, mainCredential, readOnly, @@ -577,7 +609,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil // Check permission private fun checkPermission() { if (Build.VERSION.SDK_INT in 23..28 - && !readOnly + && !mReadOnly && !mPermissionAsked) { mPermissionAsked = true // Check self permission to show or not the dialog @@ -654,7 +686,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil } private fun changeOpenFileReadIcon(togglePassword: MenuItem) { - if (readOnly) { + if (mReadOnly) { togglePassword.setTitle(R.string.menu_file_selection_read_only) togglePassword.setIcon(R.drawable.ic_read_only_white_24dp) } else { @@ -668,7 +700,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil when (item.itemId) { android.R.id.home -> finish() R.id.menu_open_file_read_mode_key -> { - readOnly = !readOnly + mReadOnly = !mReadOnly changeOpenFileReadIcon(item) } else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item) @@ -695,8 +727,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil var keyFileResult = false mExternalFileHelper?.let { - keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data - ) { uri -> + keyFileResult = it.onOpenDocumentResult(requestCode, resultCode, data) { uri -> if (uri != null) { mDatabaseKeyFileUri = uri populateKeyFileTextView(uri) @@ -706,9 +737,9 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil if (!keyFileResult) { // this block if not a key file response when (resultCode) { - LockingActivity.RESULT_EXIT_LOCK -> { + DatabaseLockActivity.RESULT_EXIT_LOCK -> { clearCredentialsViews() - Database.getInstance().clearAndClose(this) + closeDatabase() } Activity.RESULT_CANCELED -> { clearCredentialsViews() @@ -727,6 +758,7 @@ open class PasswordActivity : SpecialModeActivity(), AdvancedUnlockFragment.Buil private const val KEY_KEYFILE = "keyFile" private const val VIEW_INTENT = "android.intent.action.VIEW" + private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_PASSWORD = "password" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_PERMISSION_ASKED = "KEY_PERMISSION_ASKED" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt index 44c9f482e..13c3a9d60 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/AssignMasterKeyDialogFragment.kt @@ -32,7 +32,6 @@ import android.view.View import android.widget.CompoundButton import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper @@ -41,7 +40,7 @@ import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.view.KeyFileSelectionView -class AssignMasterKeyDialogFragment : DialogFragment() { +class AssignMasterKeyDialogFragment : DatabaseDialogFragment() { private var mMasterPassword: String? = null private var mKeyFile: Uri? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt index 6a83f0df7..e4bb7915c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseChangedDialogFragment.kt @@ -23,12 +23,11 @@ import android.app.Dialog import android.os.Bundle import android.text.SpannableStringBuilder import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.model.SnapFileDatabaseInfo -class DatabaseChangedDialogFragment : DialogFragment() { +class DatabaseChangedDialogFragment : DatabaseDialogFragment() { var actionDatabaseListener: ActionDatabaseChangedListener? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt new file mode 100644 index 000000000..1cacb3d3c --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt @@ -0,0 +1,71 @@ +package com.kunzisoft.keepass.activities.dialogs + +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval +import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.viewmodels.DatabaseViewModel + +abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { + + private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() + private var mDatabase: Database? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mDatabaseViewModel.database.observe(this) { database -> + this.mDatabase = database + resetAppTimeoutOnTouchOrFocus() + onDatabaseRetrieved(database) + } + + mDatabaseViewModel.actionFinished.observe(this) { result -> + onDatabaseActionFinished(result.database, result.actionTask, result.result) + } + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + resetAppTimeoutOnTouchOrFocus() + } + + override fun onDatabaseRetrieved(database: Database?) { + // Can be overridden by a subclass + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + // Can be overridden by a subclass + } + + fun resetAppTimeout() { + context?.let { + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(it, + mDatabase?.loaded ?: false) + } + } + + open fun overrideTimeoutTouchAndFocusEvents(): Boolean { + return false + } + + private fun resetAppTimeoutOnTouchOrFocus() { + if (!overrideTimeoutTouchAndFocusEvents()) { + context?.let { + dialog?.window?.decorView?.resetAppTimeoutWhenViewTouchedOrFocused( + it, + mDatabase?.loaded + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatePickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatePickerFragment.kt index 99abb4434..86081c301 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatePickerFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatePickerFragment.kt @@ -6,6 +6,7 @@ import android.content.Context import android.os.Bundle import androidx.fragment.app.DialogFragment +// Not as DatabaseDialogFragment because crash on KitKat class DatePickerFragment : DialogFragment() { private var mDefaultYear: Int = 2000 diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt index b278bf815..2c03c412a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DeleteNodesDialogFragment.kt @@ -20,61 +20,38 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog -import android.content.Context import android.os.Bundle import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.node.Node -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getBundleFromListNodes -import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle +import com.kunzisoft.keepass.viewmodels.NodesViewModel -open class DeleteNodesDialogFragment : DialogFragment() { +class DeleteNodesDialogFragment : DatabaseDialogFragment() { - private var mNodesToDelete: List = ArrayList() - private var mListener: DeleteNodeListener? = null - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - mListener = context as DeleteNodeListener - } catch (e: ClassCastException) { - throw ClassCastException(context.toString() - + " must implement " + DeleteNodeListener::class.java.name) - } - } - - override fun onDetach() { - mListener = null - super.onDetach() - } - - protected open fun retrieveMessage(): String { - return getString(R.string.warning_permanently_delete_nodes) - } + private var mNodesToDelete: List = listOf() + private val mNodesViewModel: NodesViewModel by activityViewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - + mNodesViewModel.nodesToDelete.observe(this) { nodes -> + this.mNodesToDelete = nodes + } + var recycleBin = false arguments?.apply { - if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY) - && containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) { - mNodesToDelete = getListNodesFromBundle(Database.getInstance(), this) - } - } ?: savedInstanceState?.apply { - if (containsKey(DatabaseTaskNotificationService.GROUPS_ID_KEY) - && containsKey(DatabaseTaskNotificationService.ENTRIES_ID_KEY)) { - mNodesToDelete = getListNodesFromBundle(Database.getInstance(), savedInstanceState) + if (containsKey(RECYCLE_BIN_TAG)) { + recycleBin = this.getBoolean(RECYCLE_BIN_TAG) } } activity?.let { activity -> // Use the Builder class for convenient dialog construction val builder = AlertDialog.Builder(activity) - builder.setMessage(retrieveMessage()) + builder.setMessage(if (recycleBin) + getString(R.string.warning_empty_recycle_bin) + else + getString(R.string.warning_permanently_delete_nodes)) builder.setPositiveButton(android.R.string.ok) { _, _ -> - mListener?.permanentlyDeleteNodes(mNodesToDelete) + mNodesViewModel.permanentlyDeleteNodes(mNodesToDelete) } builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() } // Create the AlertDialog object and return it @@ -83,19 +60,14 @@ open class DeleteNodesDialogFragment : DialogFragment() { return super.onCreateDialog(savedInstanceState) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putAll(getBundleFromListNodes(mNodesToDelete)) - } - - interface DeleteNodeListener { - fun permanentlyDeleteNodes(nodes: List) - } - companion object { - fun getInstance(nodesToDelete: List): DeleteNodesDialogFragment { + private const val RECYCLE_BIN_TAG = "RECYCLE_BIN_TAG" + + fun getInstance(recycleBin: Boolean): DeleteNodesDialogFragment { return DeleteNodesDialogFragment().apply { - arguments = getBundleFromListNodes(nodesToDelete) + arguments = Bundle().apply { + putBoolean(RECYCLE_BIN_TAG, recycleBin) + } } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EntryCustomFieldDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EntryCustomFieldDialogFragment.kt index 750afbae4..af186073c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EntryCustomFieldDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/EntryCustomFieldDialogFragment.kt @@ -31,14 +31,13 @@ import android.widget.ImageView import android.widget.TextView import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.security.ProtectedString -import com.kunzisoft.keepass.model.Field -class EntryCustomFieldDialogFragment: DialogFragment() { +class EntryCustomFieldDialogFragment: DatabaseDialogFragment() { private var oldField: Field? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt index ccb47d8f3..1114827d8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GeneratePasswordDialogFragment.kt @@ -22,18 +22,18 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog import android.content.Context import android.os.Bundle -import com.google.android.material.textfield.TextInputLayout -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog import android.view.View import android.widget.* +import androidx.appcompat.app.AlertDialog +import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.password.PasswordGenerator import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.view.applyFontVisibility -class GeneratePasswordDialogFragment : DialogFragment() { +class GeneratePasswordDialogFragment : DatabaseDialogFragment() { private var mListener: GeneratePasswordListener? = null @@ -42,6 +42,8 @@ class GeneratePasswordDialogFragment : DialogFragment() { private var passwordInputLayoutView: TextInputLayout? = null private var passwordView: EditText? = null + private var mPasswordField: Field? = null + private var uppercaseBox: CompoundButton? = null private var lowercaseBox: CompoundButton? = null private var digitsBox: CompoundButton? = null @@ -77,7 +79,7 @@ class GeneratePasswordDialogFragment : DialogFragment() { passwordView = root?.findViewById(R.id.password) passwordView?.applyFontVisibility() val passwordCopyView: ImageView? = root?.findViewById(R.id.password_copy_button) - passwordCopyView?.visibility = if(PreferencesUtil.allowCopyPasswordAndProtectedFields(activity)) + passwordCopyView?.visibility = if(PreferencesUtil.allowCopyProtectedFields(activity)) View.VISIBLE else View.GONE val clipboardHelper = ClipboardHelper(activity) passwordCopyView?.setOnClickListener { @@ -98,6 +100,8 @@ class GeneratePasswordDialogFragment : DialogFragment() { bracketsBox = root?.findViewById(R.id.cb_brackets) extendedBox = root?.findViewById(R.id.cb_extended) + mPasswordField = arguments?.getParcelable(KEY_PASSWORD_FIELD) + assignDefaultCharacters() val seekBar = root?.findViewById(R.id.seekbar_length) @@ -120,16 +124,18 @@ class GeneratePasswordDialogFragment : DialogFragment() { builder.setView(root) .setPositiveButton(R.string.accept) { _, _ -> - val bundle = Bundle() - bundle.putString(KEY_PASSWORD_ID, passwordView!!.text.toString()) - mListener?.acceptPassword(bundle) - + mPasswordField?.let { passwordField -> + passwordView?.text?.toString()?.let { passwordValue -> + passwordField.protectedValue.stringValue = passwordValue + } + mListener?.acceptPassword(passwordField) + } dismiss() } .setNegativeButton(android.R.string.cancel) { _, _ -> - val bundle = Bundle() - mListener?.cancelPassword(bundle) - + mPasswordField?.let { passwordField -> + mListener?.cancelPassword(passwordField) + } dismiss() } @@ -200,11 +206,19 @@ class GeneratePasswordDialogFragment : DialogFragment() { } interface GeneratePasswordListener { - fun acceptPassword(bundle: Bundle) - fun cancelPassword(bundle: Bundle) + fun acceptPassword(passwordField: Field) + fun cancelPassword(passwordField: Field) } companion object { - const val KEY_PASSWORD_ID = "KEY_PASSWORD_ID" + private const val KEY_PASSWORD_FIELD = "KEY_PASSWORD_FIELD" + + fun getInstance(field: Field): GeneratePasswordDialogFragment { + return GeneratePasswordDialogFragment().apply { + arguments = Bundle().apply { + putParcelable(KEY_PASSWORD_FIELD, field) + } + } + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt index e1c1085a5..bb3ffe39c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupEditDialogFragment.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog -import android.content.Context import android.graphics.Color import android.os.Bundle import android.view.View @@ -28,35 +27,34 @@ import android.widget.Button import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.IconPickerActivity -import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.CREATION -import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.UPDATE +import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment.EditGroupDialogAction.* import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.model.GroupInfo -import com.kunzisoft.keepass.view.ExpirationView +import com.kunzisoft.keepass.view.DateTimeEditFieldView +import com.kunzisoft.keepass.viewmodels.GroupEditViewModel import org.joda.time.DateTime -class GroupEditDialogFragment : DialogFragment() { +class GroupEditDialogFragment : DatabaseDialogFragment() { - private var mDatabase: Database? = null + private val mGroupEditViewModel: GroupEditViewModel by activityViewModels() - private var mEditGroupListener: EditGroupListener? = null - - private var mEditGroupDialogAction = EditGroupDialogAction.NONE + private var mPopulateIconMethod: ((ImageView, IconImage) -> Unit)? = null + private var mEditGroupDialogAction = NONE private var mGroupInfo = GroupInfo() + private var mGroupNamesNotAllowed: List? = null private lateinit var iconButtonView: ImageView - private var iconColor: Int = 0 + private var mIconColor: Int = 0 private lateinit var nameTextLayoutView: TextInputLayout private lateinit var nameTextView: TextView private lateinit var notesTextLayoutView: TextInputLayout private lateinit var notesTextView: TextView - private lateinit var expirationView: ExpirationView + private lateinit var expirationView: DateTimeEditFieldView enum class EditGroupDialogAction { CREATION, UPDATE, NONE; @@ -68,22 +66,51 @@ class GroupEditDialogFragment : DialogFragment() { } } - override fun onAttach(context: Context) { - super.onAttach(context) - // Verify that the host activity implements the callback interface - try { - // Instantiate the NoticeDialogListener so we can send events to the host - mEditGroupListener = context as EditGroupListener - } catch (e: ClassCastException) { - // The activity doesn't implement the interface, throw exception - throw ClassCastException(context.toString() - + " must implement " + GroupEditDialogFragment::class.java.name) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mGroupEditViewModel.onIconSelected.observe(this) { iconImage -> + mGroupInfo.icon = iconImage + mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) + } + + mGroupEditViewModel.onDateSelected.observe(this) { viewModelDate -> + // Save the date + mGroupInfo.expiryTime = DateInstant( + DateTime(mGroupInfo.expiryTime.date) + .withYear(viewModelDate.year) + .withMonthOfYear(viewModelDate.month + 1) + .withDayOfMonth(viewModelDate.day) + .toDate()) + expirationView.dateTime = mGroupInfo.expiryTime + if (expirationView.dateTime.type == DateInstant.Type.DATE_TIME) { + val instantTime = DateInstant(mGroupInfo.expiryTime.date, DateInstant.Type.TIME) + // Trick to recall selection with time + mGroupEditViewModel.requestDateTimeSelection(instantTime) + } + } + + mGroupEditViewModel.onTimeSelected.observe(this) { viewModelTime -> + // Save the time + mGroupInfo.expiryTime = DateInstant( + DateTime(mGroupInfo.expiryTime.date) + .withHourOfDay(viewModelTime.hours) + .withMinuteOfHour(viewModelTime.minutes) + .toDate(), mGroupInfo.expiryTime.type) + expirationView.dateTime = mGroupInfo.expiryTime + } + + mGroupEditViewModel.groupNamesNotAllowed.observe(this) { namesNotAllowed -> + this.mGroupNamesNotAllowed = namesNotAllowed } } - override fun onDetach() { - mEditGroupListener = null - super.onDetach() + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + mPopulateIconMethod = { imageView, icon -> + database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) + } + mPopulateIconMethod?.invoke(iconButtonView, mGroupInfo.icon) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -98,12 +125,9 @@ class GroupEditDialogFragment : DialogFragment() { // Retrieve the textColor to tint the icon val ta = activity.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) - iconColor = ta.getColor(0, Color.WHITE) + mIconColor = ta.getColor(0, Color.WHITE) ta.recycle() - // Init elements - mDatabase = Database.getInstance() - if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ACTION_ID) && savedInstanceState.containsKey(KEY_GROUP_INFO)) { @@ -120,32 +144,22 @@ class GroupEditDialogFragment : DialogFragment() { } // populate info in views - populateInfoToViews() - expirationView.setOnDateClickListener = { - expirationView.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(parentFragmentManager, "DatePickerFragment") - } + populateInfoToViews(mGroupInfo) + + iconButtonView.setOnClickListener { _ -> + mGroupEditViewModel.requestIconSelection(mGroupInfo.icon) + } + expirationView.setOnDateClickListener = { dateInstant -> + mGroupEditViewModel.requestDateTimeSelection(dateInstant) } val builder = AlertDialog.Builder(activity) builder.setView(root) .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel) { _, _ -> - retrieveGroupInfoFromViews() - mEditGroupListener?.cancelEditGroup( - mEditGroupDialogAction, - mGroupInfo) + // Do nothing } - iconButtonView.setOnClickListener { _ -> - IconPickerActivity.launch(activity, mGroupInfo.icon) - } - return builder.create() } return super.onCreateDialog(savedInstanceState) @@ -155,40 +169,34 @@ class GroupEditDialogFragment : DialogFragment() { super.onResume() // To prevent auto dismiss - val d = dialog as AlertDialog? - if (d != null) { - val positiveButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button + val alertDialog = dialog as AlertDialog? + if (alertDialog != null) { + val positiveButton = alertDialog.getButton(Dialog.BUTTON_POSITIVE) as Button positiveButton.setOnClickListener { retrieveGroupInfoFromViews() if (isValid()) { - mEditGroupListener?.approveEditGroup( - mEditGroupDialogAction, - mGroupInfo) - d.dismiss() + when (mEditGroupDialogAction) { + CREATION -> + mGroupEditViewModel.approveGroupCreation(mGroupInfo) + UPDATE -> + mGroupEditViewModel.approveGroupUpdate(mGroupInfo) + NONE -> {} + } + alertDialog.dismiss() } } } } - fun getExpiryTime(): DateInstant { - retrieveGroupInfoFromViews() - return mGroupInfo.expiryTime - } - - fun setExpiryTime(expiryTime: DateInstant) { - mGroupInfo.expiryTime = expiryTime - populateInfoToViews() - } - - private fun populateInfoToViews() { - assignIconView() - nameTextView.text = mGroupInfo.title - notesTextLayoutView.visibility = if (mGroupInfo.notes == null) View.GONE else View.VISIBLE - mGroupInfo.notes?.let { + private fun populateInfoToViews(groupInfo: GroupInfo) { + mGroupEditViewModel.selectIcon(groupInfo.icon) + nameTextView.text = groupInfo.title + notesTextLayoutView.visibility = if (groupInfo.notes == null) View.GONE else View.VISIBLE + groupInfo.notes?.let { notesTextView.text = it } - expirationView.expires = mGroupInfo.expires - expirationView.expiryTime = mGroupInfo.expiryTime + expirationView.activation = groupInfo.expires + expirationView.dateTime = groupInfo.expiryTime } private fun retrieveGroupInfoFromViews() { @@ -198,17 +206,8 @@ class GroupEditDialogFragment : DialogFragment() { if (newNotes.isNotEmpty()) { mGroupInfo.notes = newNotes } - mGroupInfo.expires = expirationView.expires - mGroupInfo.expiryTime = expirationView.expiryTime - } - - private fun assignIconView() { - mDatabase?.iconDrawableFactory?.assignDatabaseIcon(iconButtonView, mGroupInfo.icon, iconColor) - } - - fun setIcon(icon: IconImage) { - mGroupInfo.icon = icon - assignIconView() + mGroupInfo.expires = expirationView.activation + mGroupInfo.expiryTime = expirationView.dateTime } override fun onSaveInstanceState(outState: Bundle) { @@ -219,7 +218,21 @@ class GroupEditDialogFragment : DialogFragment() { } private fun isValid(): Boolean { - val error = mEditGroupListener?.isValidGroupName(nameTextView.text.toString()) ?: Error(false, null) + val name = nameTextView.text.toString() + val error = when { + name.isEmpty() -> { + Error(true, R.string.error_no_name) + } + mGroupNamesNotAllowed == null -> { + Error(true, R.string.error_word_reserved) + } + mGroupNamesNotAllowed?.find { it.equals(name, ignoreCase = true) } != null -> { + Error(true, R.string.error_word_reserved) + } + else -> { + Error(false, null) + } + } error.messageId?.let { messageId -> nameTextLayoutView.error = getString(messageId) } ?: kotlin.run { @@ -230,14 +243,6 @@ class GroupEditDialogFragment : DialogFragment() { data class Error(val isError: Boolean, val messageId: Int?) - interface EditGroupListener { - fun isValidGroupName(name: String): Error - fun approveEditGroup(action: EditGroupDialogAction, - groupInfo: GroupInfo) - fun cancelEditGroup(action: EditGroupDialogAction, - groupInfo: GroupInfo) - } - companion object { const val TAG_CREATE_GROUP = "TAG_CREATE_GROUP" diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt index e9f033e2d..df32460cb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/PasswordEncodingDialogFragment.kt @@ -19,11 +19,11 @@ */ package com.kunzisoft.keepass.activities.dialogs -import android.app.AlertDialog import android.app.Dialog import android.content.Context import android.net.Uri import android.os.Bundle +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.model.MainCredential diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ReplaceAttachmentDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ReplaceAttachmentDialogFragment.kt index 0c6625409..82208a213 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ReplaceAttachmentDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/ReplaceAttachmentDialogFragment.kt @@ -25,14 +25,13 @@ import android.net.Uri import android.os.Bundle import android.text.SpannableStringBuilder import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.Attachment /** * Custom Dialog to confirm big file to upload */ -class ReplaceFileDialogFragment : DialogFragment() { +class ReplaceFileDialogFragment : DatabaseDialogFragment() { private var mActionChooseListener: ActionChooseListener? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt index eb31824e8..77b18d107 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt @@ -31,7 +31,6 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.* import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.BuildConfig import com.kunzisoft.keepass.R @@ -49,7 +48,7 @@ import com.kunzisoft.keepass.otp.TokenCalculator import com.kunzisoft.keepass.utils.UriUtil import java.util.* -class SetOTPDialogFragment : DialogFragment() { +class SetOTPDialogFragment : DatabaseDialogFragment() { private var mCreateOTPElementListener: CreateOtpListener? = null @@ -80,11 +79,15 @@ class SetOTPDialogFragment : DialogFragment() { private var mOnFocusChangeListener = View.OnFocusChangeListener { _, isFocus -> if (!isFocus) mManualEvent = true + else + resetAppTimeout() } + @SuppressLint("ClickableViewAccessibility") private var mOnTouchListener = View.OnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { mManualEvent = true + resetAppTimeout() } } false @@ -95,6 +98,10 @@ class SetOTPDialogFragment : DialogFragment() { private var mPeriodWellFormed = false private var mDigitsWellFormed = false + override fun overrideTimeoutTouchAndFocusEvents(): Boolean { + return true + } + override fun onAttach(context: Context) { super.onAttach(context) // Verify that the host activity implements the callback interface @@ -225,8 +232,11 @@ class SetOTPDialogFragment : DialogFragment() { val builder = AlertDialog.Builder(activity) builder.apply { setView(root) - .setPositiveButton(android.R.string.ok) {_, _ -> } + .setPositiveButton(android.R.string.ok) { _, _ -> + resetAppTimeout() + } .setNegativeButton(android.R.string.cancel) { _, _ -> + resetAppTimeout() } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt index 970af8249..45cbb9db1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SortDialogFragment.kt @@ -22,16 +22,15 @@ package com.kunzisoft.keepass.activities.dialogs import android.app.Dialog import android.content.Context import android.os.Bundle -import androidx.annotation.IdRes -import androidx.fragment.app.DialogFragment -import androidx.appcompat.app.AlertDialog import android.view.View import android.widget.CompoundButton import android.widget.RadioGroup +import androidx.annotation.IdRes +import androidx.appcompat.app.AlertDialog import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.element.SortNodeEnum -class SortDialogFragment : DialogFragment() { +class SortDialogFragment : DatabaseDialogFragment() { private var mListener: SortSelectionListener? = null diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/TimePickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/TimePickerFragment.kt index 935d3f3b0..9e6a0ebc7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/TimePickerFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/TimePickerFragment.kt @@ -8,6 +8,7 @@ import android.os.Bundle import android.text.format.DateFormat import androidx.fragment.app.DialogFragment +// Not as DatabaseDialogFragment because crash on KitKat class TimePickerFragment : DialogFragment() { private var defaultHour: Int = 0 diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt new file mode 100644 index 000000000..671f5fff7 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt @@ -0,0 +1,51 @@ +package com.kunzisoft.keepass.activities.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval +import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.binary.BinaryData +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.viewmodels.DatabaseViewModel + +abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval { + + private val mDatabaseViewModel: DatabaseViewModel by activityViewModels() + protected var mDatabase: Database? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> + if (mDatabase == null || mDatabase != database) { + this.mDatabase = database + onDatabaseRetrieved(database) + } + } + + mDatabaseViewModel.actionFinished.observe(viewLifecycleOwner) { result -> + onDatabaseActionFinished(result.database, result.actionTask, result.result) + } + } + + protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) { + context?.let { + view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded) + } + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + // Can be overridden by a subclass + } + + protected fun buildNewBinaryAttachment(): BinaryData? { + return mDatabase?.buildNewBinaryAttachment() + } +} \ No newline at end of file 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 2c8f1a4f8..1d8bea4bf 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,431 +19,264 @@ */ package com.kunzisoft.keepass.activities.fragments -import android.content.Context import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.widget.EditText -import android.widget.ImageView +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.EntryEditActivity -import com.kunzisoft.keepass.activities.dialogs.GeneratePasswordDialogFragment -import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrChanged -import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment +import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.database.element.DateInstant -import com.kunzisoft.keepass.database.element.icon.IconImage -import com.kunzisoft.keepass.education.EntryEditActivityEducation -import com.kunzisoft.keepass.icons.IconDrawableFactory -import com.kunzisoft.keepass.model.* -import com.kunzisoft.keepass.otp.OtpEntryFields -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.view.ExpirationView -import com.kunzisoft.keepass.view.applyFontVisibility +import com.kunzisoft.keepass.database.element.template.Template +import com.kunzisoft.keepass.model.AttachmentState +import com.kunzisoft.keepass.model.EntryAttachmentState +import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.model.StreamDirection +import com.kunzisoft.keepass.view.TemplateEditView import com.kunzisoft.keepass.view.collapse import com.kunzisoft.keepass.view.expand +import com.kunzisoft.keepass.view.showByFading +import com.kunzisoft.keepass.viewmodels.EntryEditViewModel -class EntryEditFragment: StylishFragment() { - - private lateinit var entryTitleLayoutView: TextInputLayout - private lateinit var entryTitleView: EditText - private lateinit var entryIconView: ImageView - private lateinit var entryUserNameView: EditText - private lateinit var entryUrlView: EditText - private lateinit var entryPasswordLayoutView: TextInputLayout - private lateinit var entryPasswordView: EditText - private lateinit var entryPasswordGeneratorView: View - private lateinit var entryExpirationView: ExpirationView - private lateinit var entryNotesView: EditText - private lateinit var extraFieldsContainerView: View - private lateinit var extraFieldsListView: ViewGroup - private lateinit var attachmentsContainerView: View - private lateinit var attachmentsListView: RecyclerView +class EntryEditFragment: DatabaseFragment() { - private lateinit var attachmentsAdapter: EntryAttachmentsItemsAdapter + private val mEntryEditViewModel: EntryEditViewModel by activityViewModels() - private var fontInVisibility: Boolean = false - private var iconColor: Int = 0 + private lateinit var rootView: View + private lateinit var templateView: TemplateEditView + private lateinit var attachmentsContainerView: ViewGroup + private lateinit var attachmentsListView: RecyclerView + private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null - var drawFactory: IconDrawableFactory? = null - var setOnDateClickListener: (() -> Unit)? = null - var setOnPasswordGeneratorClickListener: View.OnClickListener? = null - var setOnIconViewClickListener: ((IconImage) -> Unit)? = null - var setOnEditCustomField: ((Field) -> Unit)? = null - var setOnRemoveAttachment: ((Attachment) -> Unit)? = null + private var mTemplate: Template? = null + private var mAllowMultipleAttachments: Boolean = false - // Elements to modify the current entry - private var mEntryInfo = EntryInfo() - private var mLastFocusedEditField: FocusedEditField? = null - private var mExtraViewToRequestFocus: EditText? = null + private var mIconColor: Int = 0 - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) - val rootView = inflater.cloneInContext(contextThemed) - .inflate(R.layout.fragment_entry_edit_contents, container, false) + // Retrieve the textColor to tint the icon + val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) + mIconColor = taIconColor?.getColor(0, Color.BLACK) ?: Color.BLACK + taIconColor?.recycle() - fontInVisibility = PreferencesUtil.fieldFontIsInVisibility(requireContext()) + return inflater.cloneInContext(contextThemed) + .inflate(R.layout.fragment_entry_edit, container, false) + } - entryTitleLayoutView = rootView.findViewById(R.id.entry_edit_container_title) - entryTitleView = rootView.findViewById(R.id.entry_edit_title) - entryIconView = rootView.findViewById(R.id.entry_edit_icon_button) - entryIconView.setOnClickListener { - setOnIconViewClickListener?.invoke(mEntryInfo.icon) - } + override fun onViewCreated(view: View, + savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) - entryUserNameView = rootView.findViewById(R.id.entry_edit_user_name) - entryUrlView = rootView.findViewById(R.id.entry_edit_url) - entryPasswordLayoutView = rootView.findViewById(R.id.entry_edit_container_password) - entryPasswordView = rootView.findViewById(R.id.entry_edit_password) - entryPasswordGeneratorView = rootView.findViewById(R.id.entry_edit_password_generator_button) - entryPasswordGeneratorView.setOnClickListener { - setOnPasswordGeneratorClickListener?.onClick(it) + rootView = view + // Hide only the first time + if (savedInstanceState == null) { + view.isVisible = false } - entryExpirationView = rootView.findViewById(R.id.entry_edit_expiration) - entryExpirationView.setOnDateClickListener = setOnDateClickListener + templateView = view.findViewById(R.id.template_view) + attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) + attachmentsListView = view.findViewById(R.id.entry_attachments_list) - entryNotesView = rootView.findViewById(R.id.entry_edit_notes) - - extraFieldsContainerView = rootView.findViewById(R.id.extra_fields_container) - extraFieldsListView = rootView.findViewById(R.id.extra_fields_list) - - attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) - attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) - // TODO retrieve current database with its unique key - attachmentsAdapter.database = Database.getInstance() - //attachmentsAdapter.database = arguments?.getInt(KEY_DATABASE) - attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize -> - if (previousSize > 0 && newSize == 0) { - attachmentsContainerView.collapse(true) - } else if (previousSize == 0 && newSize == 1) { - attachmentsContainerView.expand(true) - } - } attachmentsListView.apply { layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) adapter = attachmentsAdapter (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false } - // Retrieve the textColor to tint the icon - val taIconColor = contextThemed?.theme?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) - iconColor = taIconColor?.getColor(0, Color.WHITE) ?: Color.WHITE - taIconColor?.recycle() - - rootView?.resetAppTimeoutWhenViewFocusedOrChanged(requireContext()) - - // Retrieve the new entry after an orientation change - if (arguments?.containsKey(KEY_TEMP_ENTRY_INFO) == true) - mEntryInfo = arguments?.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo - else if (savedInstanceState?.containsKey(KEY_TEMP_ENTRY_INFO) == true) { - mEntryInfo = savedInstanceState.getParcelable(KEY_TEMP_ENTRY_INFO) ?: mEntryInfo + templateView.apply { + setOnIconClickListener { + mEntryEditViewModel.requestIconSelection(templateView.getIcon()) + } + setOnCustomEditionActionClickListener { field -> + mEntryEditViewModel.requestCustomFieldEdition(field) + } + setOnPasswordGenerationActionClickListener { field -> + mEntryEditViewModel.requestPasswordSelection(field) + } + setOnDateInstantClickListener { dateInstant -> + mEntryEditViewModel.requestDateTimeSelection(dateInstant) + } } - if (savedInstanceState?.containsKey(KEY_LAST_FOCUSED_FIELD) == true) { - mLastFocusedEditField = savedInstanceState.getParcelable(KEY_LAST_FOCUSED_FIELD) ?: mLastFocusedEditField + if (savedInstanceState != null) { + val attachments: List = + savedInstanceState.getParcelableArrayList(ATTACHMENTS_TAG) ?: listOf() + setAttachments(attachments) } - populateViewsWithEntry() - - return rootView - } - - override fun onDetach() { - super.onDetach() - - drawFactory = null - setOnDateClickListener = null - setOnPasswordGeneratorClickListener = null - setOnIconViewClickListener = null - setOnRemoveAttachment = null - setOnEditCustomField = null - } - - fun getEntryInfo(): EntryInfo { - populateEntryWithViews() - return mEntryInfo - } + mEntryEditViewModel.onTemplateChanged.observe(viewLifecycleOwner) { template -> + this.mTemplate = template + templateView.setTemplate(template) + } - fun generatePasswordEducationPerformed(entryEditActivityEducation: EntryEditActivityEducation): Boolean { - return entryEditActivityEducation.checkAndPerformedGeneratePasswordEducation( - entryPasswordGeneratorView, - { - GeneratePasswordDialogFragment().show(parentFragmentManager, "PasswordGeneratorFragment") - }, - { - try { - (activity as? EntryEditActivity?)?.performedNextEducation(entryEditActivityEducation) - } catch (ignore: Exception) {} + mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry -> + if (templateEntry != null) { + val selectedTemplate = if (mTemplate != null) + mTemplate + else + templateEntry.defaultTemplate + templateView.setTemplate(selectedTemplate) + // Load entry info only the first time to keep change locally + if (savedInstanceState == null) { + assignEntryInfo(templateEntry.entryInfo) } - ) - } - - private fun populateViewsWithEntry() { - // Set info in view - icon = mEntryInfo.icon - title = mEntryInfo.title - username = mEntryInfo.username - url = mEntryInfo.url - password = mEntryInfo.password - expires = mEntryInfo.expires - expiryTime = mEntryInfo.expiryTime - notes = mEntryInfo.notes - assignExtraFields(mEntryInfo.customFields) { fields -> - setOnEditCustomField?.invoke(fields) - } - assignAttachments(mEntryInfo.attachments, StreamDirection.UPLOAD) { attachment -> - setOnRemoveAttachment?.invoke(attachment) + // To prevent flickering + rootView.showByFading() + // Apply timeout reset + resetAppTimeoutWhenViewFocusedOrChanged(rootView) + } } - } - - private fun populateEntryWithViews() { - // Icon already populate - mEntryInfo.title = title - mEntryInfo.username = username - mEntryInfo.url = url - mEntryInfo.password = password - mEntryInfo.expires = expires - mEntryInfo.expiryTime = expiryTime - mEntryInfo.notes = notes - mEntryInfo.customFields = getExtraFields() - mEntryInfo.otpModel = OtpEntryFields.parseFields { key -> - getExtraFields().firstOrNull { it.name == key }?.protectedValue?.toString() - }?.otpModel - mEntryInfo.attachments = getAttachments() - } - var title: String - get() { - return entryTitleView.text.toString() - } - set(value) { - entryTitleView.setText(value) - if (fontInVisibility) - entryTitleView.applyFontVisibility() + mEntryEditViewModel.requestEntryInfoUpdate.observe(viewLifecycleOwner) { + mEntryEditViewModel.saveEntryInfo(it.database, it.entry, it.parent, retrieveEntryInfo()) } - var icon: IconImage - get() { - return mEntryInfo.icon - } - set(value) { - mEntryInfo.icon = value - drawFactory?.assignDatabaseIcon(entryIconView, value, iconColor) + mEntryEditViewModel.onIconSelected.observe(viewLifecycleOwner) { iconImage -> + templateView.setIcon(iconImage) } - var username: String - get() { - return entryUserNameView.text.toString() - } - set(value) { - entryUserNameView.setText(value) - if (fontInVisibility) - entryUserNameView.applyFontVisibility() + mEntryEditViewModel.onPasswordSelected.observe(viewLifecycleOwner) { passwordField -> + templateView.setPasswordField(passwordField) } - var url: String - get() { - return entryUrlView.text.toString() - } - set(value) { - entryUrlView.setText(value) - if (fontInVisibility) - entryUrlView.applyFontVisibility() + mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate -> + // Save the date + templateView.setCurrentDateTimeValue(viewModelDate) } - var password: String - get() { - return entryPasswordView.text.toString() - } - set(value) { - entryPasswordView.setText(value) - if (fontInVisibility) { - entryPasswordView.applyFontVisibility() - } + mEntryEditViewModel.onTimeSelected.observe(viewLifecycleOwner) { viewModelTime -> + // Save the time + templateView.setCurrentTimeValue(viewModelTime) } - var expires: Boolean - get() { - return entryExpirationView.expires - } - set(value) { - entryExpirationView.expires = value + mEntryEditViewModel.onCustomFieldEdited.observe(viewLifecycleOwner) { fieldAction -> + val oldField = fieldAction.oldField + val newField = fieldAction.newField + // Field to add + if (oldField == null) { + newField?.let { + if (!templateView.putCustomField(it)) { + mEntryEditViewModel.showCustomFieldEditionError() + } + } + } + // Field to replace + oldField?.let { + newField?.let { + if (!templateView.replaceCustomField(oldField, newField)) { + mEntryEditViewModel.showCustomFieldEditionError() + } + } + } + // Field to remove + if (newField == null) { + oldField?.let { + templateView.removeCustomField(it) + } + } } - var expiryTime: DateInstant - get() { - return entryExpirationView.expiryTime - } - set(value) { - entryExpirationView.expiryTime = value + mEntryEditViewModel.requestSetupOtp.observe(viewLifecycleOwner) { + // Retrieve the current otpElement if exists + // and open the dialog to set up the OTP + SetOTPDialogFragment.build(templateView.getEntryInfo().otpModel) + .show(parentFragmentManager, "addOTPDialog") } - var notes: String - get() { - return entryNotesView.text.toString() + mEntryEditViewModel.onOtpCreated.observe(viewLifecycleOwner) { + // Update the otp field with otpauth:// url + templateView.putOtpElement(it) } - set(value) { - entryNotesView.setText(value) - if (fontInVisibility) - entryNotesView.applyFontVisibility() - } - - /* ------------- - * Extra Fields - * ------------- - */ - private var mExtraFieldsList: MutableList = ArrayList() - private var mOnEditButtonClickListener: ((item: Field)->Unit)? = null + mEntryEditViewModel.onBuildNewAttachment.observe(viewLifecycleOwner) { + val attachmentToUploadUri = it.attachmentToUploadUri + val fileName = it.fileName - private fun buildViewFromField(extraField: Field): View? { - val inflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? - val itemView: View? = inflater?.inflate(R.layout.item_entry_edit_extra_field, extraFieldsListView, false) - itemView?.id = View.NO_ID - - val extraFieldValueContainer: TextInputLayout? = itemView?.findViewById(R.id.entry_extra_field_value_container) - extraFieldValueContainer?.endIconMode = if (extraField.protectedValue.isProtected) - TextInputLayout.END_ICON_PASSWORD_TOGGLE else TextInputLayout.END_ICON_NONE - extraFieldValueContainer?.hint = extraField.name - extraFieldValueContainer?.id = View.NO_ID - - val extraFieldValue: TextInputEditText? = itemView?.findViewById(R.id.entry_extra_field_value) - extraFieldValue?.apply { - if (extraField.protectedValue.isProtected) { - inputType = extraFieldValue.inputType or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD + buildNewBinaryAttachment()?.let { binaryAttachment -> + val entryAttachment = Attachment(fileName, binaryAttachment) + // Ask to replace the current attachment + if ((!mAllowMultipleAttachments + && containsAttachment()) || + containsAttachment(EntryAttachmentState(entryAttachment, StreamDirection.UPLOAD))) { + ReplaceFileDialogFragment.build(attachmentToUploadUri, entryAttachment) + .show(parentFragmentManager, "replacementFileFragment") + } else { + mEntryEditViewModel.startUploadAttachment(attachmentToUploadUri, entryAttachment) + } } - setText(extraField.protectedValue.toString()) - if (fontInVisibility) - applyFontVisibility() - } - extraFieldValue?.id = View.NO_ID - extraFieldValue?.tag = "FIELD_VALUE_TAG" - if (mLastFocusedEditField?.field == extraField) { - mExtraViewToRequestFocus = extraFieldValue - } - - val extraFieldEditButton: View? = itemView?.findViewById(R.id.entry_extra_field_edit) - extraFieldEditButton?.setOnClickListener { - mOnEditButtonClickListener?.invoke(extraField) } - extraFieldEditButton?.id = View.NO_ID - return itemView - } - - fun getExtraFields(): List { - mLastFocusedEditField = null - for (index in 0 until extraFieldsListView.childCount) { - val extraFieldValue: EditText = extraFieldsListView.getChildAt(index) - .findViewWithTag("FIELD_VALUE_TAG") - val extraField = mExtraFieldsList[index] - extraField.protectedValue.stringValue = extraFieldValue.text?.toString() ?: "" - if (extraFieldValue.isFocused) { - mLastFocusedEditField = FocusedEditField().apply { - field = extraField - cursorSelectionStart = extraFieldValue.selectionStart - cursorSelectionEnd = extraFieldValue.selectionEnd + mEntryEditViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState -> + when (entryAttachmentState?.downloadState) { + AttachmentState.START -> { + putAttachment(entryAttachmentState) + getAttachmentViewPosition(entryAttachmentState) { attachment, position -> + mEntryEditViewModel.binaryPreviewLoaded(attachment, position) + } + } + AttachmentState.IN_PROGRESS -> { + putAttachment(entryAttachmentState) + } + AttachmentState.COMPLETE -> { + putAttachment(entryAttachmentState) { entryAttachment -> + getAttachmentViewPosition(entryAttachment) { attachment, position -> + mEntryEditViewModel.binaryPreviewLoaded(attachment, position) + } + } + mEntryEditViewModel.onAttachmentAction(null) } + AttachmentState.CANCELED, + AttachmentState.ERROR -> { + removeAttachment(entryAttachmentState) + mEntryEditViewModel.onAttachmentAction(null) + } + else -> {} } } - return mExtraFieldsList } - /** - * Remove all children and add new views for each field - */ - fun assignExtraFields(fields: List, - onEditButtonClickListener: ((item: Field)->Unit)?) { - extraFieldsContainerView.visibility = if (fields.isEmpty()) View.GONE else View.VISIBLE - // Reinit focused field - mExtraFieldsList.clear() - mExtraFieldsList.addAll(fields) - extraFieldsListView.removeAllViews() - fields.forEach { - extraFieldsListView.addView(buildViewFromField(it)) + override fun onDatabaseRetrieved(database: Database?) { + + templateView.populateIconMethod = { imageView, icon -> + database?.iconDrawableFactory?.assignDatabaseIcon(imageView, icon, mIconColor) } - // Request last focus - mLastFocusedEditField?.let { focusField -> - mExtraViewToRequestFocus?.apply { - requestFocus() - setSelection(focusField.cursorSelectionStart, - focusField.cursorSelectionEnd) + + mAllowMultipleAttachments = database?.allowMultipleAttachments == true + + attachmentsAdapter?.database = database + attachmentsAdapter?.onListSizeChangedListener = { previousSize, newSize -> + if (previousSize > 0 && newSize == 0) { + attachmentsContainerView.collapse(true) + } else if (previousSize == 0 && newSize == 1) { + attachmentsContainerView.expand(true) } } - mLastFocusedEditField = null - mOnEditButtonClickListener = onEditButtonClickListener } - /** - * Update an extra field or create a new one if doesn't exists, the old value is lost - */ - fun putExtraField(extraField: Field) { - extraFieldsContainerView.visibility = View.VISIBLE - val oldField = mExtraFieldsList.firstOrNull { it.name == extraField.name } - oldField?.let { - val index = mExtraFieldsList.indexOf(oldField) - mExtraFieldsList.removeAt(index) - mExtraFieldsList.add(index, extraField) - extraFieldsListView.removeViewAt(index) - val newView = buildViewFromField(extraField) - extraFieldsListView.addView(newView, index) - newView?.requestFocus() - } ?: kotlin.run { - mExtraFieldsList.add(extraField) - val newView = buildViewFromField(extraField) - extraFieldsListView.addView(newView) - newView?.requestFocus() - } - } + private fun assignEntryInfo(entryInfo: EntryInfo?) { + // Populate entry views + templateView.setEntryInfo(entryInfo) - /** - * Update an extra field and keep the old value - */ - fun replaceExtraField(oldExtraField: Field, newExtraField: Field) { - extraFieldsContainerView.visibility = View.VISIBLE - val index = mExtraFieldsList.indexOf(oldExtraField) - val oldValueEditText: EditText = extraFieldsListView.getChildAt(index) - .findViewWithTag("FIELD_VALUE_TAG") - val oldValue = oldValueEditText.text.toString() - val newExtraFieldWithOldValue = Field(newExtraField).apply { - this.protectedValue.stringValue = oldValue - } - mExtraFieldsList.removeAt(index) - mExtraFieldsList.add(index, newExtraFieldWithOldValue) - extraFieldsListView.removeViewAt(index) - val newView = buildViewFromField(newExtraFieldWithOldValue) - extraFieldsListView.addView(newView, index) - newView?.requestFocus() + // Manage attachments + setAttachments(entryInfo?.attachments ?: listOf()) } - fun removeExtraField(oldExtraField: Field) { - val previousSize = mExtraFieldsList.size - val index = mExtraFieldsList.indexOf(oldExtraField) - extraFieldsListView.getChildAt(index)?.let { - it.collapse(true) { - mExtraFieldsList.removeAt(index) - extraFieldsListView.removeViewAt(index) - val newSize = mExtraFieldsList.size - - if (previousSize > 0 && newSize == 0) { - extraFieldsContainerView.collapse(true) - } else if (previousSize == 0 && newSize == 1) { - extraFieldsContainerView.expand(true) - } - } - } + private fun retrieveEntryInfo(): EntryInfo { + val entryInfo = templateView.getEntryInfo() + entryInfo.attachments = getAttachments().toMutableList() + return entryInfo } /* ------------- @@ -451,78 +284,84 @@ class EntryEditFragment: StylishFragment() { * ------------- */ - fun getAttachments(): List { - return attachmentsAdapter.itemsList.map { it.attachment } + private fun getAttachments(): List { + return attachmentsAdapter?.itemsList?.map { it.attachment } ?: listOf() } - fun assignAttachments(attachments: List, - streamDirection: StreamDirection, - onDeleteItem: (attachment: Attachment)->Unit) { + private fun setAttachments(attachments: List) { attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE - attachmentsAdapter.assignItems(attachments.map { EntryAttachmentState(it, streamDirection) }) - attachmentsAdapter.onDeleteButtonClickListener = { item -> - onDeleteItem.invoke(item.attachment) + attachmentsAdapter?.assignItems(attachments.map { + EntryAttachmentState(it, StreamDirection.UPLOAD) + }) + attachmentsAdapter?.onDeleteButtonClickListener = { item -> + val attachment = item.attachment + removeAttachment(EntryAttachmentState(attachment, StreamDirection.DOWNLOAD)) + mEntryEditViewModel.deleteAttachment(attachment) } } - fun containsAttachment(): Boolean { - return !attachmentsAdapter.isEmpty() + private fun containsAttachment(): Boolean { + return attachmentsAdapter?.isEmpty() != true } - fun containsAttachment(attachment: EntryAttachmentState): Boolean { - return attachmentsAdapter.contains(attachment) + private fun containsAttachment(attachment: EntryAttachmentState): Boolean { + return attachmentsAdapter?.contains(attachment) ?: false } - fun putAttachment(attachment: EntryAttachmentState, - onPreviewLoaded: (()-> Unit)? = null) { + private fun putAttachment(attachment: EntryAttachmentState, + onPreviewLoaded: ((attachment: EntryAttachmentState) -> Unit)? = null) { + // When only one attachment is allowed + if (!mAllowMultipleAttachments + && attachment.downloadState == AttachmentState.START) { + attachmentsAdapter?.clear() + } attachmentsContainerView.visibility = View.VISIBLE - attachmentsAdapter.putItem(attachment) - attachmentsAdapter.onBinaryPreviewLoaded = { - onPreviewLoaded?.invoke() + attachmentsAdapter?.putItem(attachment) + attachmentsAdapter?.onBinaryPreviewLoaded = { + onPreviewLoaded?.invoke(attachment) } } - fun removeAttachment(attachment: EntryAttachmentState) { - attachmentsAdapter.removeItem(attachment) - } - - fun clearAttachments() { - attachmentsAdapter.clear() + private fun removeAttachment(attachment: EntryAttachmentState) { + attachmentsAdapter?.removeItem(attachment) } - fun getAttachmentViewPosition(attachment: EntryAttachmentState, position: (Float) -> Unit) { + private fun getAttachmentViewPosition(attachment: EntryAttachmentState, + position: (attachment: EntryAttachmentState, Float) -> Unit) { attachmentsListView.postDelayed({ - position.invoke(attachmentsContainerView.y - + attachmentsListView.y - + (attachmentsListView.getChildAt(attachmentsAdapter.indexOf(attachment))?.y - ?: 0F) - ) + attachmentsAdapter?.indexOf(attachment)?.let { index -> + position.invoke(attachment, + attachmentsContainerView.y + + attachmentsListView.y + + (attachmentsListView.getChildAt(index)?.y + ?: 0F) + ) + } }, 250) } override fun onSaveInstanceState(outState: Bundle) { - populateEntryWithViews() - outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo) - outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField) - super.onSaveInstanceState(outState) + outState.putParcelableArrayList(ATTACHMENTS_TAG, ArrayList(getAttachments())) + } + + /* ------------- + * Education + * ------------- + */ + + fun getActionImageView(): View? { + return templateView.getActionImageView() + } + + fun launchGeneratePasswordEductionAction() { + mEntryEditViewModel.requestPasswordSelection(templateView.getPasswordField()) } companion object { - const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" - const val KEY_DATABASE = "KEY_DATABASE" - const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD" - - fun getInstance(entryInfo: EntryInfo?): EntryEditFragment { - //database: Database?): EntryEditFragment { - return EntryEditFragment().apply { - arguments = Bundle().apply { - putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo) - // TODO Unique database key database.key - putInt(KEY_DATABASE, 0) - } - } - } + private val TAG = EntryEditFragment::class.java.name + + private const val ATTACHMENTS_TAG = "ATTACHMENTS_TAG" } } \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt new file mode 100644 index 000000000..00e89a9a2 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt @@ -0,0 +1,253 @@ +package com.kunzisoft.keepass.activities.fragments + +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter +import com.kunzisoft.keepass.database.element.Attachment +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.DateInstant +import com.kunzisoft.keepass.database.element.template.TemplateField +import com.kunzisoft.keepass.model.EntryAttachmentState +import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.model.StreamDirection +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.timeout.ClipboardHelper +import com.kunzisoft.keepass.utils.UuidUtil +import com.kunzisoft.keepass.view.TemplateView +import com.kunzisoft.keepass.view.showByFading +import com.kunzisoft.keepass.viewmodels.EntryViewModel +import java.util.* + +class EntryFragment: DatabaseFragment() { + + private lateinit var rootView: View + private lateinit var templateView: TemplateView + + private lateinit var creationDateView: TextView + private lateinit var modificationDateView: TextView + + private lateinit var attachmentsContainerView: View + private lateinit var attachmentsListView: RecyclerView + private var attachmentsAdapter: EntryAttachmentsItemsAdapter? = null + + private lateinit var uuidContainerView: View + private lateinit var uuidView: TextView + private lateinit var uuidReferenceView: TextView + + private var mClipboardHelper: ClipboardHelper? = null + + private val mEntryViewModel: EntryViewModel by activityViewModels() + + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + + return inflater.cloneInContext(contextThemed) + .inflate(R.layout.fragment_entry, container, false) + } + + override fun onViewCreated(view: View, + savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + context?.let { context -> + mClipboardHelper = ClipboardHelper(context) + } + + rootView = view + // Hide only the first time + if (savedInstanceState == null) { + view.isVisible = false + } + templateView = view.findViewById(R.id.entry_template) + loadTemplateSettings() + + attachmentsContainerView = view.findViewById(R.id.entry_attachments_container) + attachmentsListView = view.findViewById(R.id.entry_attachments_list) + attachmentsListView.apply { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + creationDateView = view.findViewById(R.id.entry_created) + modificationDateView = view.findViewById(R.id.entry_modified) + + uuidContainerView = view.findViewById(R.id.entry_UUID_container) + uuidContainerView.apply { + visibility = if (PreferencesUtil.showUUID(context)) View.VISIBLE else View.GONE + } + uuidView = view.findViewById(R.id.entry_UUID) + uuidReferenceView = view.findViewById(R.id.entry_UUID_reference) + + mEntryViewModel.entryInfoHistory.observe(viewLifecycleOwner) { entryInfoHistory -> + if (entryInfoHistory != null) { + templateView.setTemplate(entryInfoHistory.template) + assignEntryInfo(entryInfoHistory.entryInfo) + // Smooth appearing + rootView.showByFading() + resetAppTimeoutWhenViewFocusedOrChanged(rootView) + } + } + + mEntryViewModel.onAttachmentAction.observe(viewLifecycleOwner) { entryAttachmentState -> + entryAttachmentState?.let { + if (it.streamDirection != StreamDirection.UPLOAD) { + putAttachment(it) + } + } + } + } + + override fun onDatabaseRetrieved(database: Database?) { + context?.let { context -> + attachmentsAdapter = EntryAttachmentsItemsAdapter(context) + attachmentsAdapter?.database = database + } + + attachmentsListView.adapter = attachmentsAdapter + } + + private fun loadTemplateSettings() { + context?.let { context -> + templateView.setFirstTimeAskAllowCopyProtectedFields(PreferencesUtil.isFirstTimeAskAllowCopyProtectedFields(context)) + templateView.setAllowCopyProtectedFields(PreferencesUtil.allowCopyProtectedFields(context)) + } + } + + private fun assignEntryInfo(entryInfo: EntryInfo?) { + // Set copy buttons + templateView.apply { + setOnAskCopySafeClickListener { + showClipboardDialog() + } + + setOnCopyActionClickListener { field -> + mClipboardHelper?.timeoutCopyToClipboard( + field.protectedValue.stringValue, + getString( + R.string.copy_field, + TemplateField.getLocalizedName(context, field.name) + ) + ) + } + } + + // Populate entry views + templateView.setEntryInfo(entryInfo) + + // OTP timer updated + templateView.setOnOtpElementUpdated { otpElementUpdated -> + mEntryViewModel.onOtpElementUpdated(otpElementUpdated) + } + + // Manage attachments + assignAttachments(entryInfo?.attachments ?: listOf()) + + // Assign dates + assignCreationDate(entryInfo?.creationTime) + assignModificationDate(entryInfo?.lastModificationTime) + + // Assign special data + assignUUID(entryInfo?.id) + } + + private fun showClipboardDialog() { + context?.let { + AlertDialog.Builder(it) + .setMessage( + getString(R.string.allow_copy_password_warning) + + "\n\n" + + getString(R.string.clipboard_warning) + ) + .create().apply { + setButton(AlertDialog.BUTTON_POSITIVE, getText(R.string.enable)) { dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, true) + finishDialog(dialog) + } + setButton(AlertDialog.BUTTON_NEGATIVE, getText(R.string.disable)) { dialog, _ -> + PreferencesUtil.setAllowCopyPasswordAndProtectedFields(context, false) + finishDialog(dialog) + } + show() + } + } + } + + private fun finishDialog(dialog: DialogInterface) { + dialog.dismiss() + loadTemplateSettings() + templateView.reload() + } + + private fun assignCreationDate(date: DateInstant?) { + creationDateView.text = date?.getDateTimeString(resources) + } + + private fun assignModificationDate(date: DateInstant?) { + modificationDateView.text = date?.getDateTimeString(resources) + } + + private fun assignUUID(uuid: UUID?) { + uuidView.text = uuid?.toString() + uuidReferenceView.text = UuidUtil.toHexString(uuid) + } + + /* ------------- + * Attachments + * ------------- + */ + + private fun assignAttachments(attachments: List) { + attachmentsContainerView.visibility = if (attachments.isEmpty()) View.GONE else View.VISIBLE + attachmentsAdapter?.assignItems(attachments.map { + EntryAttachmentState(it, StreamDirection.DOWNLOAD) + }) + attachmentsAdapter?.onItemClickListener = { item -> + mEntryViewModel.onAttachmentSelected(item.attachment) + } + } + + fun putAttachment(attachmentToDownload: EntryAttachmentState) { + attachmentsAdapter?.putItem(attachmentToDownload) + } + + /* ------------- + * Education + * ------------- + */ + + fun firstEntryFieldCopyView(): View? { + return try { + templateView.getActionImageView() + } catch (e: Exception) { + null + } + } + + fun launchEntryCopyEducationAction() { + val appNameString = getString(R.string.app_name) + mClipboardHelper?.timeoutCopyToClipboard(appNameString, + getString(R.string.copy_field, appNameString)) + } + + companion object { + + fun getInstance(): EntryFragment { + return EntryFragment().apply { + arguments = Bundle() + } + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryHistoryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryHistoryFragment.kt new file mode 100644 index 000000000..a270e8e90 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryHistoryFragment.kt @@ -0,0 +1,72 @@ +package com.kunzisoft.keepass.activities.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.stylish.StylishFragment +import com.kunzisoft.keepass.adapters.EntryHistoryAdapter +import com.kunzisoft.keepass.model.EntryInfo +import com.kunzisoft.keepass.viewmodels.EntryViewModel + +class EntryHistoryFragment: StylishFragment() { + + private lateinit var historyContainerView: View + private lateinit var historyListView: RecyclerView + private var historyAdapter: EntryHistoryAdapter? = null + + private val mEntryViewModel: EntryViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + super.onCreateView(inflater, container, savedInstanceState) + + return inflater.cloneInContext(contextThemed) + .inflate(R.layout.fragment_entry_history, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + context?.let { context -> + historyAdapter = EntryHistoryAdapter(context) + } + + historyContainerView = view.findViewById(R.id.entry_history_container) + historyListView = view.findViewById(R.id.entry_history_list) + historyListView.apply { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) + adapter = historyAdapter + } + + mEntryViewModel.entryHistory.observe(viewLifecycleOwner) { + assignHistory(it) + } + } + + /* ------------- + * History + * ------------- + */ + private fun assignHistory(history: List?) { + historyAdapter?.clear() + history?.let { + historyAdapter?.entryHistoryList?.addAll(history) + } + historyAdapter?.onItemClickListener = { item, position -> + mEntryViewModel.onHistorySelected(item, position) + } + historyContainerView.visibility = if (historyAdapter?.entryHistoryList?.isEmpty() != false) + View.GONE + else + View.VISIBLE + historyAdapter?.notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt similarity index 59% rename from app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt index d44f49de8..39d5b3b86 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/ListNodesFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt @@ -25,34 +25,40 @@ import android.os.Bundle import android.util.Log import android.view.* import androidx.appcompat.view.ActionMode +import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.EntryEditActivity import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper -import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.adapters.NodeAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.viewmodels.GroupViewModel import java.util.* -class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionListener { +class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListener { private var nodeClickListener: NodeClickListener? = null private var onScrollListener: OnScrollListener? = null private var mNodesRecyclerView: RecyclerView? = null - var mainGroup: Group? = null - private set + private var mLayoutManager: LinearLayoutManager? = null private var mAdapter: NodeAdapter? = null + private val mGroupViewModel: GroupViewModel by activityViewModels() + + private var mCurrentGroup: Group? = null + var nodeActionSelectionMode = false private set var nodeActionPasteMode: PasteMode = PasteMode.UNDEFINED @@ -63,12 +69,23 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis private var notFoundView: View? = null private var isASearchResult: Boolean = false - - private var readOnly: Boolean = false private var specialMode: SpecialMode = SpecialMode.DEFAULT - val isEmpty: Boolean - get() = mAdapter == null || mAdapter?.itemCount?:0 <= 0 + private var mRecycleBinEnable: Boolean = false + private var mRecycleBin: Group? = null + + private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + super.onScrollStateChanged(recyclerView, newState) + if (newState == SCROLL_STATE_IDLE) { + mGroupViewModel.assignPosition(getFirstVisiblePosition()) + } + } + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + onScrollListener?.onScrolled(dy) + } + } override fun onAttach(context: Context) { super.onAttach(context) @@ -100,131 +117,138 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis super.onCreate(savedInstanceState) setHasOptionsMenu(true) + } - readOnly = ReadOnlyHelper.retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState, arguments) - - arguments?.let { args -> - // Contains all the group in element - if (args.containsKey(GROUP_KEY)) { - mainGroup = args.getParcelable(GROUP_KEY) - } - if (args.containsKey(IS_SEARCH)) { - isASearchResult = args.getBoolean(IS_SEARCH) - } - } + override fun onDatabaseRetrieved(database: Database?) { + mRecycleBinEnable = database?.isRecycleBinEnabled == true + mRecycleBin = database?.recycleBin contextThemed?.let { context -> - mAdapter = NodeAdapter(context) - mAdapter?.apply { - setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { - override fun onNodeClick(node: Node) { - if (nodeActionSelectionMode) { - if (listActionNodes.contains(node)) { - // Remove selected item if already selected - listActionNodes.remove(node) + database?.let { database -> + mAdapter = NodeAdapter(context, database).apply { + setOnNodeClickListener(object : NodeAdapter.NodeClickCallback { + override fun onNodeClick(database: Database, node: Node) { + if (nodeActionSelectionMode) { + if (listActionNodes.contains(node)) { + // Remove selected item if already selected + listActionNodes.remove(node) + } else { + // Add selected item if not already selected + listActionNodes.add(node) + } + nodeClickListener?.onNodeSelected(database, listActionNodes) + setActionNodes(listActionNodes) + notifyNodeChanged(node) } else { - // Add selected item if not already selected - listActionNodes.add(node) + nodeClickListener?.onNodeClick(database, node) } - nodeClickListener?.onNodeSelected(listActionNodes) - setActionNodes(listActionNodes) - notifyNodeChanged(node) - } else { - nodeClickListener?.onNodeClick(node) } - } - override fun onNodeLongClick(node: Node): Boolean { - if (nodeActionPasteMode == PasteMode.UNDEFINED) { - // Select the first item after a long click - if (!listActionNodes.contains(node)) - listActionNodes.add(node) + override fun onNodeLongClick(database: Database, node: Node): Boolean { + if (nodeActionPasteMode == PasteMode.UNDEFINED) { + // Select the first item after a long click + if (!listActionNodes.contains(node)) + listActionNodes.add(node) - nodeClickListener?.onNodeSelected(listActionNodes) + nodeClickListener?.onNodeSelected(database, listActionNodes) - setActionNodes(listActionNodes) - notifyNodeChanged(node) + setActionNodes(listActionNodes) + notifyNodeChanged(node) + } + return true } - return true - } - }) + }) + } + mNodesRecyclerView?.adapter = mAdapter } } } - override fun onSaveInstanceState(outState: Bundle) { - ReadOnlyHelper.onSaveInstanceState(outState, readOnly) - super.onSaveInstanceState(outState) + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + + // Too many special cases to make specific additions or deletions, + // rebuilt the list works well. + if (result.isSuccess) { + rebuildList() + } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) - // To apply theme - val rootView = inflater.cloneInContext(contextThemed) - .inflate(R.layout.fragment_list_nodes, container, false) - mNodesRecyclerView = rootView.findViewById(R.id.nodes_list) - notFoundView = rootView.findViewById(R.id.not_found_container) + return inflater.cloneInContext(contextThemed) + .inflate(R.layout.fragment_group, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mNodesRecyclerView = view.findViewById(R.id.nodes_list) + notFoundView = view.findViewById(R.id.not_found_container) + + mLayoutManager = LinearLayoutManager(context) mNodesRecyclerView?.apply { scrollBarStyle = View.SCROLLBARS_INSIDE_INSET - layoutManager = LinearLayoutManager(context) + layoutManager = mLayoutManager adapter = mAdapter } + resetAppTimeoutWhenViewFocusedOrChanged(view) - onScrollListener?.let { onScrollListener -> - mNodesRecyclerView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - onScrollListener.onScrolled(dy) - } - }) + mGroupViewModel.group.observe(viewLifecycleOwner) { + mCurrentGroup = it.group + isASearchResult = it.group.isVirtual + rebuildList() + it.showFromPosition?.let { position -> + mNodesRecyclerView?.scrollToPosition(position) + } } - - return rootView } override fun onResume() { super.onResume() + mNodesRecyclerView?.addOnScrollListener(mRecycleViewScrollListener) activity?.intent?.let { specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) } - // Refresh data + rebuildList() + } + + override fun onPause() { + + mNodesRecyclerView?.removeOnScrollListener(mRecycleViewScrollListener) + super.onPause() + } + + fun getFirstVisiblePosition(): Int { + return mLayoutManager?.findFirstVisibleItemPosition() ?: 0 + } + + private fun rebuildList() { try { - rebuildList() - } catch (e: Exception) { - Log.e(TAG, "Unable to rebuild the list during resume") - e.printStackTrace() + // Add elements to the list + mCurrentGroup?.let { mainGroup -> + // Thrown an exception when sort cannot be performed + mAdapter?.rebuildList(mainGroup) + } + } catch (e:Exception) { + Log.e(TAG, "Unable to rebuild the list", e) } - if (isASearchResult && mAdapter!= null && mAdapter!!.isEmpty) { + if (isASearchResult && mAdapter != null && mAdapter!!.isEmpty) { // To show the " no search entry found " - mNodesRecyclerView?.visibility = View.GONE notFoundView?.visibility = View.VISIBLE } else { - mNodesRecyclerView?.visibility = View.VISIBLE notFoundView?.visibility = View.GONE } } - @Throws(IllegalArgumentException::class) - fun rebuildList() { - // Add elements to the list - mainGroup?.let { mainGroup -> - mAdapter?.apply { - // Thrown an exception when sort cannot be performed - rebuildList(mainGroup) - // To visually change the elements - if (PreferencesUtil.APPEARANCE_CHANGED) { - notifyDataSetChanged() - PreferencesUtil.APPEARANCE_CHANGED = false - } - } - } - } - override fun onSortSelected(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { // Save setting @@ -237,8 +261,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis mAdapter?.notifyChangeSort(sortNodeEnum, sortNodeParameters) rebuildList() } catch (e:Exception) { - Log.e(TAG, "Unable to rebuild the list with the sort") - e.printStackTrace() + Log.e(TAG, "Unable to sort the list", e) } } @@ -254,7 +277,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis R.id.menu_sort -> { context?.let { context -> val sortDialogFragment: SortDialogFragment = - if (Database.getInstance().isRecycleBinEnabled) { + if (mRecycleBinEnable) { SortDialogFragment.getInstance( PreferencesUtil.getListSort(context), PreferencesUtil.getAscendingSort(context), @@ -276,34 +299,32 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } } - fun actionNodesCallback(nodes: List, + fun actionNodesCallback(database: Database, + nodes: List, menuListener: NodesActionMenuListener?, - actionModeCallback: ActionMode.Callback) : ActionMode.Callback { + onDestroyActionMode: (mode: ActionMode?) -> Unit) : ActionMode.Callback { return object : ActionMode.Callback { override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { nodeActionSelectionMode = false nodeActionPasteMode = PasteMode.UNDEFINED - return actionModeCallback.onCreateActionMode(mode, menu) + return true } override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { menu?.clear() - if (nodeActionPasteMode != PasteMode.UNDEFINED) { mode?.menuInflater?.inflate(R.menu.node_paste_menu, menu) } else { nodeActionSelectionMode = true mode?.menuInflater?.inflate(R.menu.node_menu, menu) - val database = Database.getInstance() - // Open and Edit for a single item if (nodes.size == 1) { // Edition - if (readOnly - || (database.isRecycleBinEnabled && nodes[0] == database.recycleBin)) { + if (database.isReadOnly + || (mRecycleBinEnable && nodes[0] == mRecycleBin)) { menu?.removeItem(R.id.menu_edit) } } else { @@ -312,59 +333,58 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } // Move - if (readOnly + if (database.isReadOnly || isASearchResult) { menu?.removeItem(R.id.menu_move) } // Copy (not allowed for group) - if (readOnly + if (database.isReadOnly || isASearchResult || nodes.any { it.type == Type.GROUP }) { menu?.removeItem(R.id.menu_copy) } // Deletion - if (readOnly - || (database.isRecycleBinEnabled && nodes.any { it == database.recycleBin })) { + if (database.isReadOnly + || (mRecycleBinEnable && nodes.any { it == mRecycleBin })) { menu?.removeItem(R.id.menu_delete) } } // Add the number of items selected in title mode?.title = nodes.size.toString() - - return actionModeCallback.onPrepareActionMode(mode, menu) + return true } override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { if (menuListener == null) return false return when (item?.itemId) { - R.id.menu_open -> menuListener.onOpenMenuClick(nodes[0]) - R.id.menu_edit -> menuListener.onEditMenuClick(nodes[0]) + R.id.menu_open -> menuListener.onOpenMenuClick(database, nodes[0]) + R.id.menu_edit -> menuListener.onEditMenuClick(database, nodes[0]) R.id.menu_copy -> { nodeActionPasteMode = PasteMode.PASTE_FROM_COPY mAdapter?.unselectActionNodes() - val returnValue = menuListener.onCopyMenuClick(nodes) + val returnValue = menuListener.onCopyMenuClick(database, nodes) nodeActionSelectionMode = false returnValue } R.id.menu_move -> { nodeActionPasteMode = PasteMode.PASTE_FROM_MOVE mAdapter?.unselectActionNodes() - val returnValue = menuListener.onMoveMenuClick(nodes) + val returnValue = menuListener.onMoveMenuClick(database, nodes) nodeActionSelectionMode = false returnValue } - R.id.menu_delete -> menuListener.onDeleteMenuClick(nodes) + R.id.menu_delete -> menuListener.onDeleteMenuClick(database, nodes) R.id.menu_paste -> { - val returnValue = menuListener.onPasteMenuClick(nodeActionPasteMode, nodes) + val returnValue = menuListener.onPasteMenuClick(database, nodeActionPasteMode, nodes) nodeActionPasteMode = PasteMode.UNDEFINED nodeActionSelectionMode = false returnValue } - else -> actionModeCallback.onActionItemClicked(mode, item) + else -> false } } @@ -374,7 +394,7 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis mAdapter?.unselectActionNodes() nodeActionPasteMode = PasteMode.UNDEFINED nodeActionSelectionMode = false - actionModeCallback.onDestroyActionMode(mode) + onDestroyActionMode(mode) } } } @@ -384,73 +404,40 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis when (requestCode) { EntryEditActivity.ADD_OR_UPDATE_ENTRY_REQUEST_CODE -> { - if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE - || resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) { - data?.getParcelableExtra(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { changedNode -> - if (resultCode == EntryEditActivity.ADD_ENTRY_RESULT_CODE) - addNode(changedNode) - if (resultCode == EntryEditActivity.UPDATE_ENTRY_RESULT_CODE) - mAdapter?.notifyDataSetChanged() - } ?: Log.e(this.javaClass.name, "New node can be retrieve in Activity Result") + if (resultCode == EntryEditActivity.ADD_OR_UPDATE_ENTRY_RESULT_CODE) { + data?.getParcelableExtra>(EntryEditActivity.ADD_OR_UPDATE_ENTRY_KEY)?.let { + // Simply refresh the list + rebuildList() + // Scroll to the new entry + mDatabase?.getEntryById(it)?.let { entry -> + mAdapter?.indexOf(entry)?.let { position -> + mNodesRecyclerView?.scrollToPosition(position) + } + } + } ?: Log.e(this.javaClass.name, "Entry cannot be retrieved in Activity Result") } } } } - fun contains(node: Node): Boolean { - return mAdapter?.contains(node) ?: false - } - - fun addNode(newNode: Node) { - mAdapter?.addNode(newNode) - } - - fun addNodes(newNodes: List) { - mAdapter?.addNodes(newNodes) - } - - fun updateNode(oldNode: Node, newNode: Node? = null) { - mAdapter?.updateNode(oldNode, newNode ?: oldNode) - } - - fun updateNodes(oldNodes: List, newNodes: List) { - mAdapter?.updateNodes(oldNodes, newNodes) - } - - fun removeNode(pwNode: Node) { - mAdapter?.removeNode(pwNode) - } - - fun removeNodes(nodes: List) { - mAdapter?.removeNodes(nodes) - } - - fun removeNodeAt(position: Int) { - mAdapter?.removeNodeAt(position) - } - - fun removeNodesAt(positions: IntArray) { - mAdapter?.removeNodesAt(positions) - } - /** * Callback listener to redefine to do an action when a node is click */ interface NodeClickListener { - fun onNodeClick(node: Node) - fun onNodeSelected(nodes: List): Boolean + fun onNodeClick(database: Database, node: Node) + fun onNodeSelected(database: Database, nodes: List): Boolean } /** * Menu listener to redefine to do an action in menu */ interface NodesActionMenuListener { - fun onOpenMenuClick(node: Node): Boolean - fun onEditMenuClick(node: Node): Boolean - fun onCopyMenuClick(nodes: List): Boolean - fun onMoveMenuClick(nodes: List): Boolean - fun onDeleteMenuClick(nodes: List): Boolean - fun onPasteMenuClick(pasteMode: PasteMode?, nodes: List): Boolean + fun onOpenMenuClick(database: Database, node: Node): Boolean + fun onEditMenuClick(database: Database, node: Node): Boolean + fun onCopyMenuClick(database: Database, nodes: List): Boolean + fun onMoveMenuClick(database: Database, nodes: List): Boolean + fun onDeleteMenuClick(database: Database, nodes: List): Boolean + fun onPasteMenuClick(database: Database, pasteMode: PasteMode?, nodes: List): Boolean } enum class PasteMode { @@ -469,22 +456,6 @@ class ListNodesFragment : StylishFragment(), SortDialogFragment.SortSelectionLis } companion object { - - private val TAG = ListNodesFragment::class.java.name - - private const val GROUP_KEY = "GROUP_KEY" - private const val IS_SEARCH = "IS_SEARCH" - - fun newInstance(group: Group?, readOnly: Boolean, isASearch: Boolean): ListNodesFragment { - val bundle = Bundle() - if (group != null) { - bundle.putParcelable(GROUP_KEY, group) - } - bundle.putBoolean(IS_SEARCH, isASearch) - ReadOnlyHelper.putReadOnlyInBundle(bundle, readOnly) - val listNodesFragment = ListNodesFragment() - listNodesFragment.arguments = bundle - return listNodesFragment - } + private val TAG = GroupFragment::class.java.name } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt index 281fb5451..f1f9ed87c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconCustomFragment.kt @@ -22,6 +22,7 @@ package com.kunzisoft.keepass.activities.fragments import android.os.Bundle import android.view.View import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImageCustom @@ -31,8 +32,8 @@ class IconCustomFragment : IconFragment() { return R.layout.fragment_icon_grid } - override fun defineIconList() { - mDatabase?.doForEachCustomIcons { customIcon, _ -> + override fun defineIconList(database: Database?) { + database?.doForEachCustomIcons { customIcon, _ -> iconPickerAdapter.addIcon(customIcon, false) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt index 6f067c3cc..f63a7645c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconFragment.kt @@ -19,7 +19,6 @@ */ package com.kunzisoft.keepass.activities.fragments -import android.content.Context import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater @@ -28,7 +27,6 @@ import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.adapters.IconPickerAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImageDraw @@ -38,39 +36,48 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -abstract class IconFragment : StylishFragment(), +abstract class IconFragment : DatabaseFragment(), IconPickerAdapter.IconPickerListener { protected lateinit var iconsGridView: RecyclerView protected lateinit var iconPickerAdapter: IconPickerAdapter protected var iconActionSelectionMode = false - protected var mDatabase: Database? = null - protected val iconPickerViewModel: IconPickerViewModel by activityViewModels() abstract fun retrieveMainLayoutId(): Int - abstract fun defineIconList() + abstract fun defineIconList(database: Database?) - override fun onAttach(context: Context) { - super.onAttach(context) + override fun onCreateView(inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?): View { + return inflater.inflate(retrieveMainLayoutId(), container, false) + } - mDatabase = Database.getInstance() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) // Retrieve the textColor to tint the icon val ta = contextThemed?.obtainStyledAttributes(intArrayOf(android.R.attr.textColor)) val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK ta?.recycle() - iconPickerAdapter = IconPickerAdapter(context, tintColor).apply { - iconDrawableFactory = mDatabase?.iconDrawableFactory - } + iconsGridView = view.findViewById(R.id.icons_grid_view) + iconPickerAdapter = IconPickerAdapter(requireContext(), tintColor) + iconPickerAdapter.iconPickerListener = this + iconsGridView.adapter = iconPickerAdapter + + resetAppTimeoutWhenViewFocusedOrChanged(view) + } + + override fun onDatabaseRetrieved(database: Database?) { + iconPickerAdapter.iconDrawableFactory = database?.iconDrawableFactory CoroutineScope(Dispatchers.IO).launch { val populateList = launch { iconPickerAdapter.clear() - defineIconList() + defineIconList(database) } withContext(Dispatchers.Main) { populateList.join() @@ -79,21 +86,6 @@ abstract class IconFragment : StylishFragment(), } } - override fun onCreateView(inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?): View { - val root = inflater.inflate(retrieveMainLayoutId(), container, false) - iconsGridView = root.findViewById(R.id.icons_grid_view) - iconsGridView.adapter = iconPickerAdapter - return root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - iconPickerAdapter.iconPickerListener = this - } - fun onIconDeleteClicked() { iconActionSelectionMode = false } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt index ba14e7dea..e056d89f1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconPickerFragment.kt @@ -9,20 +9,18 @@ import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.adapters.IconPickerPagerAdapter import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.viewmodels.IconPickerViewModel -class IconPickerFragment : StylishFragment() { +class IconPickerFragment : DatabaseFragment() { private var iconPickerPagerAdapter: IconPickerPagerAdapter? = null private lateinit var viewPager: ViewPager2 + private lateinit var tabLayout: TabLayout private val iconPickerViewModel: IconPickerViewModel by activityViewModels() - private var mDatabase: Database? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -32,19 +30,11 @@ class IconPickerFragment : StylishFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - mDatabase = Database.getInstance() + super.onViewCreated(view, savedInstanceState) viewPager = view.findViewById(R.id.icon_picker_pager) - val tabLayout = view.findViewById(R.id.icon_picker_tabs) - iconPickerPagerAdapter = IconPickerPagerAdapter(this, - if (mDatabase?.allowCustomIcons == true) 2 else 1) - viewPager.adapter = iconPickerPagerAdapter - TabLayoutMediator(tabLayout, viewPager) { tab, position -> - tab.text = when (position) { - 1 -> getString(R.string.icon_section_custom) - else -> getString(R.string.icon_section_standard) - } - }.attach() + tabLayout = view.findViewById(R.id.icon_picker_tabs) + resetAppTimeoutWhenViewFocusedOrChanged(view) arguments?.apply { if (containsKey(ICON_TAB_ARG)) { @@ -58,6 +48,18 @@ class IconPickerFragment : StylishFragment() { } } + override fun onDatabaseRetrieved(database: Database?) { + iconPickerPagerAdapter = IconPickerPagerAdapter(this, + if (database?.allowCustomIcons == true) 2 else 1) + viewPager.adapter = iconPickerPagerAdapter + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + tab.text = when (position) { + 1 -> getString(R.string.icon_section_custom) + else -> getString(R.string.icon_section_standard) + } + }.attach() + } + enum class IconTab { STANDARD, CUSTOM } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt index 62807e174..dd16578c4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/IconStandardFragment.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.activities.fragments import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.icon.IconImageStandard @@ -29,8 +30,8 @@ class IconStandardFragment : IconFragment() { return R.layout.fragment_icon_grid } - override fun defineIconList() { - mDatabase?.doForEachStandardIcons { standardIcon -> + override fun defineIconList(database: Database?) { + database?.doForEachStandardIcons { standardIcon -> iconPickerAdapter.addIcon(standardIcon, false) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt index 9dbfd3a97..2792c35ef 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ExternalFileHelper.kt @@ -127,17 +127,7 @@ class ExternalFileHelper { if (data != null) { val uri = data.data if (uri != null) { - try { - // try to persist read and write permissions - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - activity?.contentResolver?.apply { - takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - } - } - } catch (e: Exception) { - // nop - } + UriUtil.takeUriPermission(activity?.contentResolver, uri) onFileSelected?.invoke(uri) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ReadOnlyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ReadOnlyHelper.kt deleted file mode 100644 index 5203b987e..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/ReadOnlyHelper.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.activities.helpers - -import android.content.Context -import android.content.Intent -import android.os.Bundle - -import com.kunzisoft.keepass.settings.PreferencesUtil - -object ReadOnlyHelper { - - private const val READ_ONLY_KEY = "READ_ONLY_KEY" - - const val READ_ONLY_DEFAULT = false - - fun retrieveReadOnlyFromIntent(intent: Intent): Boolean { - return intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT) - } - - fun retrieveReadOnlyFromInstanceStateOrPreference(context: Context, savedInstanceState: Bundle?): Boolean { - return if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) { - savedInstanceState.getBoolean(READ_ONLY_KEY) - } else { - PreferencesUtil.enableReadOnlyDatabase(context) - } - } - - fun retrieveReadOnlyFromInstanceStateOrArguments(savedInstanceState: Bundle?, arguments: Bundle?): Boolean { - var readOnly = READ_ONLY_DEFAULT - if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) { - readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY) - } else if (arguments != null && arguments.containsKey(READ_ONLY_KEY)) { - readOnly = arguments.getBoolean(READ_ONLY_KEY) - } - return readOnly - } - - fun retrieveReadOnlyFromInstanceStateOrIntent(savedInstanceState: Bundle?, intent: Intent?): Boolean { - var readOnly = READ_ONLY_DEFAULT - if (savedInstanceState != null && savedInstanceState.containsKey(READ_ONLY_KEY)) { - readOnly = savedInstanceState.getBoolean(READ_ONLY_KEY) - } else { - if (intent != null) - readOnly = intent.getBooleanExtra(READ_ONLY_KEY, READ_ONLY_DEFAULT) - } - return readOnly - } - - fun putReadOnlyInIntent(intent: Intent, readOnly: Boolean) { - intent.putExtra(READ_ONLY_KEY, readOnly) - } - - fun putReadOnlyInBundle(bundle: Bundle, readOnly: Boolean) { - bundle.putBoolean(READ_ONLY_KEY, readOnly) - } - - fun onSaveInstanceState(outState: Bundle, readOnly: Boolean) { - outState.putBoolean(READ_ONLY_KEY, readOnly) - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt new file mode 100644 index 000000000..5f62542de --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt @@ -0,0 +1,80 @@ +package com.kunzisoft.keepass.activities.legacy + +import android.net.Uri +import android.os.Bundle +import androidx.activity.viewModels +import com.kunzisoft.keepass.activities.stylish.StylishActivity +import com.kunzisoft.keepass.app.database.CipherDatabaseEntity +import com.kunzisoft.keepass.database.action.DatabaseTaskProvider +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.viewmodels.DatabaseViewModel + +abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval { + + protected val mDatabaseViewModel: DatabaseViewModel by viewModels() + protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null + protected var mDatabase: Database? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mDatabaseTaskProvider = DatabaseTaskProvider(this) + + mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> + val databaseWasReloaded = database?.wasReloaded == true + if (databaseWasReloaded && finishActivityIfReloadRequested()) { + finish() + } else if (mDatabase == null || mDatabase != database || databaseWasReloaded) { + database?.wasReloaded = false + onDatabaseRetrieved(database) + } + } + mDatabaseTaskProvider?.onActionFinish = { database, actionTask, result -> + onDatabaseActionFinished(database, actionTask, result) + } + } + + override fun onDatabaseRetrieved(database: Database?) { + mDatabase = database + mDatabaseViewModel.defineDatabase(database) + // optional method implementation + } + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + mDatabaseViewModel.onActionFinished(database, actionTask, result) + // optional method implementation + } + + fun createDatabase(databaseUri: Uri, + mainCredential: MainCredential) { + mDatabaseTaskProvider?.startDatabaseCreate(databaseUri, mainCredential) + } + + fun loadDatabase(databaseUri: Uri, + mainCredential: MainCredential, + readOnly: Boolean, + cipherEntity: CipherDatabaseEntity?, + fixDuplicateUuid: Boolean) { + mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEntity, fixDuplicateUuid) + } + + protected fun closeDatabase() { + mDatabase?.clearAndClose(this) + } + + override fun onResume() { + super.onResume() + mDatabaseTaskProvider?.registerProgressTask() + } + + override fun onPause() { + mDatabaseTaskProvider?.unregisterProgressTask() + super.onPause() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt new file mode 100644 index 000000000..b4d4d7a9f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -0,0 +1,479 @@ +/* + * Copyright 2019 Jeremy Jamet / Kunzisoft. + * + * This file is part of KeePassDX. + * + * KeePassDX is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * KeePassDX is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with KeePassDX. If not, see . + * + */ +package com.kunzisoft.keepass.activities.legacy + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.viewModels +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment +import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment +import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper +import com.kunzisoft.keepass.activities.helpers.SpecialMode +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.Group +import com.kunzisoft.keepass.database.element.node.Node +import com.kunzisoft.keepass.database.element.node.NodeId +import com.kunzisoft.keepass.icons.IconDrawableFactory +import com.kunzisoft.keepass.model.GroupInfo +import com.kunzisoft.keepass.model.MainCredential +import com.kunzisoft.keepass.services.DatabaseTaskNotificationService +import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.tasks.ActionRunnable +import com.kunzisoft.keepass.timeout.TimeoutHelper +import com.kunzisoft.keepass.utils.* +import com.kunzisoft.keepass.view.showActionErrorIfNeeded +import com.kunzisoft.keepass.viewmodels.NodesViewModel +import java.util.* + +abstract class DatabaseLockActivity : DatabaseModeActivity(), + PasswordEncodingDialogFragment.Listener { + + private val mNodesViewModel: NodesViewModel by viewModels() + + protected var mTimeoutEnable: Boolean = true + + private var mLockReceiver: LockReceiver? = null + private var mExitLock: Boolean = false + + protected var mDatabaseReadOnly: Boolean = true + private var mAutoSaveEnable: Boolean = true + + protected var mIconDrawableFactory: IconDrawableFactory? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState != null + && savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY) + ) { + mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY) + } else { + if (intent != null) + mTimeoutEnable = + intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT) + } + + mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes -> + deleteDatabaseNodes(nodes) + } + + mDatabaseViewModel.saveDatabase.observe(this) { save -> + mDatabaseTaskProvider?.startDatabaseSave(save) + } + + mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> + mDatabaseTaskProvider?.startDatabaseReload(fixDuplicateUuid) + } + + mDatabaseViewModel.saveName.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveDescription.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveDescription(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveDefaultUsername.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveName(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveColor.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveColor(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveCompression.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveCompression(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.removeUnlinkData.observe(this) { + mDatabaseTaskProvider?.startDatabaseRemoveUnlinkedData(it) + } + + mDatabaseViewModel.saveRecycleBin.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveRecycleBin(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveTemplatesGroup.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveTemplatesGroup(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveMaxHistoryItems.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveMaxHistoryItems(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveMaxHistorySize.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveMaxHistorySize(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveEncryption.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveEncryption(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveKeyDerivation.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveKeyDerivation(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveIterations.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveIterations(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveMemoryUsage.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveMemoryUsage(it.oldValue, it.newValue, it.save) + } + + mDatabaseViewModel.saveParallelism.observe(this) { + mDatabaseTaskProvider?.startDatabaseSaveParallelism(it.oldValue, it.newValue, it.save) + } + + mExitLock = false + } + + open fun finishActivityIfDatabaseNotLoaded(): Boolean { + return true + } + + override fun onDatabaseRetrieved(database: Database?) { + super.onDatabaseRetrieved(database) + + // End activity if database not loaded + if (finishActivityIfDatabaseNotLoaded() && (database == null || !database.loaded)) { + finish() + } + + // Focus view to reinitialize timeout, + // view is not necessary loaded so retry later in resume + viewToInvalidateTimeout() + ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded) + + database?.let { + // check timeout + if (mTimeoutEnable) { + if (mLockReceiver == null) { + mLockReceiver = LockReceiver { + mDatabase = null + closeDatabase(database) + if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) + LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE + // Add onActivityForResult response + setResult(RESULT_EXIT_LOCK) + closeOptionsMenu() + finish() + } + registerLockReceiver(mLockReceiver) + } + + // After the first creation + // or If simply swipe with another application + // If the time is out -> close the Activity + TimeoutHelper.checkTimeAndLockIfTimeout(this) + // If onCreate already record time + if (!mExitLock) + TimeoutHelper.recordTime(this, database.loaded) + } + + mDatabaseReadOnly = database.isReadOnly + mIconDrawableFactory = database.iconDrawableFactory + + checkRegister() + } + } + + abstract fun viewToInvalidateTimeout(): View? + + override fun onDatabaseActionFinished( + database: Database, + actionTask: String, + result: ActionRunnable.Result + ) { + super.onDatabaseActionFinished(database, actionTask, result) + when (actionTask) { + DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> { + // Reload the current activity + if (result.isSuccess) { + reloadActivity() + } else { + this.showActionErrorIfNeeded(result) + finish() + } + } + } + } + + override fun onPasswordEncodingValidateListener(databaseUri: Uri?, + mainCredential: MainCredential) { + assignDatabasePassword(databaseUri, mainCredential) + } + + private fun assignDatabasePassword(databaseUri: Uri?, + mainCredential: MainCredential) { + if (databaseUri != null) { + mDatabaseTaskProvider?.startDatabaseAssignPassword(databaseUri, mainCredential) + } + } + + fun assignPassword(mainCredential: MainCredential) { + mDatabase?.let { database -> + database.fileUri?.let { databaseUri -> + // Show the progress dialog now or after dialog confirmation + if (database.validatePasswordEncoding(mainCredential)) { + assignDatabasePassword(databaseUri, mainCredential) + } else { + PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential) + .show(supportFragmentManager, "passwordEncodingTag") + } + } + } + } + + fun saveDatabase() { + mDatabaseTaskProvider?.startDatabaseSave(true) + } + + fun reloadDatabase() { + mDatabaseTaskProvider?.startDatabaseReload(false) + } + + fun createEntry(newEntry: Entry, + parent: Group) { + mDatabaseTaskProvider?.startDatabaseCreateEntry(newEntry, parent, mAutoSaveEnable) + } + + fun updateEntry(oldEntry: Entry, + entryToUpdate: Entry) { + mDatabaseTaskProvider?.startDatabaseUpdateEntry(oldEntry, entryToUpdate, mAutoSaveEnable) + } + + fun copyNodes(nodesToCopy: List, + newParent: Group) { + mDatabaseTaskProvider?.startDatabaseCopyNodes(nodesToCopy, newParent, mAutoSaveEnable) + } + + fun moveNodes(nodesToMove: List, + newParent: Group) { + mDatabaseTaskProvider?.startDatabaseMoveNodes(nodesToMove, newParent, mAutoSaveEnable) + } + + private fun eachNodeRecyclable(database: Database, nodes: List): Boolean { + return nodes.find { node -> + var cannotRecycle = true + if (node is Entry) { + cannotRecycle = !database.canRecycle(node) + } else if (node is Group) { + cannotRecycle = !database.canRecycle(node) + } + cannotRecycle + } == null + } + + fun deleteNodes(nodes: List, recycleBin: Boolean = false) { + mDatabase?.let { database -> + // If recycle bin enabled, ensure it exists + if (database.isRecycleBinEnabled) { + database.ensureRecycleBinExists(resources) + } + + // If recycle bin enabled and not in recycle bin, move in recycle bin + if (eachNodeRecyclable(database, nodes)) { + deleteDatabaseNodes(nodes) + } + // else open the dialog to confirm deletion + else { + DeleteNodesDialogFragment.getInstance(recycleBin) + .show(supportFragmentManager, "deleteNodesDialogFragment") + mNodesViewModel.deleteNodes(nodes) + } + } + } + + private fun deleteDatabaseNodes(nodes: List) { + mDatabaseTaskProvider?.startDatabaseDeleteNodes(nodes, mAutoSaveEnable) + } + + fun createGroup(parent: Group, + groupInfo: GroupInfo?) { + // Build the group + mDatabase?.createGroup()?.let { newGroup -> + groupInfo?.let { info -> + newGroup.setGroupInfo(info) + } + // Not really needed here because added in runnable but safe + newGroup.parent = parent + mDatabaseTaskProvider?.startDatabaseCreateGroup(newGroup, parent, mAutoSaveEnable) + } + } + + fun updateGroup(oldGroup: Group, + groupInfo: GroupInfo) { + // If group updated save it in the database + val updateGroup = Group(oldGroup).let { updateGroup -> + updateGroup.apply { + // WARNING remove parent and children to keep memory + removeParent() + removeChildren() + this.setGroupInfo(groupInfo) + } + } + mDatabaseTaskProvider?.startDatabaseUpdateGroup(oldGroup, updateGroup, mAutoSaveEnable) + } + + fun restoreEntryHistory(mainEntryId: NodeId, + entryHistoryPosition: Int) { + mDatabaseTaskProvider + ?.startDatabaseRestoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) + } + + fun deleteEntryHistory(mainEntryId: NodeId, + entryHistoryPosition: Int) { + mDatabaseTaskProvider?.startDatabaseDeleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_EXIT_LOCK) { + mExitLock = true + lockAndExit() + } + } + + private fun checkRegister() { + // If in ave or registration mode, don't allow read only + if ((mSpecialMode == SpecialMode.SAVE + || mSpecialMode == SpecialMode.REGISTRATION) + && mDatabaseReadOnly) { + Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() + EntrySelectionHelper.removeModesFromIntent(intent) + finish() + } + } + + override fun onResume() { + super.onResume() + + // To refresh when back to normal workflow from selection workflow + mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this) + + // Invalidate timeout by touch + mDatabase?.let { database -> + viewToInvalidateTimeout() + ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded) + } + + invalidateOptionsMenu() + + LOCKING_ACTIVITY_UI_VISIBLE = true + } + + protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) { + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this, + mDatabase?.loaded == true, + action) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable) + super.onSaveInstanceState(outState) + } + + override fun onPause() { + LOCKING_ACTIVITY_UI_VISIBLE = false + + super.onPause() + + if (mTimeoutEnable) { + // If the time is out during our navigation in activity -> close the Activity + TimeoutHelper.checkTimeAndLockIfTimeout(this) + } + } + + override fun onDestroy() { + unregisterLockReceiver(mLockReceiver) + super.onDestroy() + } + + protected fun lockAndExit() { + sendBroadcast(Intent(LOCK_ACTION)) + } + + fun resetAppTimeout() { + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this, + mDatabase?.loaded ?: false) + } + + override fun onBackPressed() { + if (mTimeoutEnable) { + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this, + mDatabase?.loaded == true) { + super.onBackPressed() + } + } else { + super.onBackPressed() + } + } + + companion object { + + const val TAG = "LockingActivity" + + const val RESULT_EXIT_LOCK = 1450 + + const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" + const val TIMEOUT_ENABLE_KEY_DEFAULT = true + + private var LOCKING_ACTIVITY_UI_VISIBLE = false + var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null + } +} + +/** + * To reset the app timeout when a view is focused or changed + */ +@SuppressLint("ClickableViewAccessibility") +fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) { + // Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout") + setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + // Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout") + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context, + databaseLoaded ?: false) + } + } + false + } + setOnFocusChangeListener { _, _ -> + // Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout") + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context, + databaseLoaded ?: false) + } + if (this is ViewGroup) { + for (i in 0..childCount) { + getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded) + } + } +} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt similarity index 96% rename from app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt rename to app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt index bc9ce9365..ae7959ba1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/selection/SpecialModeActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt @@ -1,4 +1,4 @@ -package com.kunzisoft.keepass.activities.selection +package com.kunzisoft.keepass.activities.legacy import android.os.Bundle import android.view.View @@ -7,15 +7,14 @@ import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.SpecialMode import com.kunzisoft.keepass.activities.helpers.TypeMode -import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.view.SpecialModeView /** - * Activity to manage special mode (ie: selection mode) + * Activity to manage database special mode (ie: selection mode) */ -abstract class SpecialModeActivity : StylishActivity() { +abstract class DatabaseModeActivity : DatabaseActivity() { protected var mSpecialMode: SpecialMode = SpecialMode.DEFAULT private var mTypeMode: TypeMode = TypeMode.DEFAULT diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt new file mode 100644 index 000000000..52a474dec --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseRetrieval.kt @@ -0,0 +1,11 @@ +package com.kunzisoft.keepass.activities.legacy + +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.tasks.ActionRunnable + +interface DatabaseRetrieval { + fun onDatabaseRetrieved(database: Database?) + fun onDatabaseActionFinished(database: Database, + actionTask: String, + result: ActionRunnable.Result) +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt deleted file mode 100644 index 1747edbbe..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/activities/lock/LockingActivity.kt +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2019 Jeremy Jamet / Kunzisoft. - * - * This file is part of KeePassDX. - * - * KeePassDX is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * KeePassDX is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with KeePassDX. If not, see . - * - */ -package com.kunzisoft.keepass.activities.lock - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper -import com.kunzisoft.keepass.activities.helpers.ReadOnlyHelper -import com.kunzisoft.keepass.activities.helpers.SpecialMode -import com.kunzisoft.keepass.activities.selection.SpecialModeActivity -import com.kunzisoft.keepass.database.action.ProgressDatabaseTaskProvider -import com.kunzisoft.keepass.database.element.Database -import com.kunzisoft.keepass.settings.PreferencesUtil -import com.kunzisoft.keepass.timeout.TimeoutHelper -import com.kunzisoft.keepass.utils.* - -abstract class LockingActivity : SpecialModeActivity() { - - protected var mTimeoutEnable: Boolean = true - - private var mLockReceiver: LockReceiver? = null - private var mExitLock: Boolean = false - - // Force readOnly if Entry Selection mode - protected var mReadOnly: Boolean - get() { - return mReadOnlyToSave - } - set(value) { - mReadOnlyToSave = value - } - private var mReadOnlyToSave: Boolean = false - protected var mAutoSaveEnable: Boolean = true - - var mProgressDatabaseTaskProvider: ProgressDatabaseTaskProvider? = null - private set - - override fun onCreate(savedInstanceState: Bundle?) { - - mProgressDatabaseTaskProvider = ProgressDatabaseTaskProvider(this) - - super.onCreate(savedInstanceState) - - if (savedInstanceState != null - && savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)) { - mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY) - } else { - if (intent != null) - mTimeoutEnable = intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT) - } - - if (mTimeoutEnable) { - mLockReceiver = LockReceiver { - closeDatabase() - if (LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK == null) - LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK = LOCKING_ACTIVITY_UI_VISIBLE - // Add onActivityForResult response - setResult(RESULT_EXIT_LOCK) - closeOptionsMenu() - finish() - } - registerLockReceiver(mLockReceiver) - } - - mExitLock = false - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (resultCode == RESULT_EXIT_LOCK) { - mExitLock = true - if (Database.getInstance().loaded) { - lockAndExit() - } - } - } - - override fun onResume() { - super.onResume() - - // If in ave or registration mode, don't allow read only - if ((mSpecialMode == SpecialMode.SAVE - || mSpecialMode == SpecialMode.REGISTRATION) - && mReadOnly) { - Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show() - EntrySelectionHelper.removeModesFromIntent(intent) - finish() - } - - mProgressDatabaseTaskProvider?.registerProgressTask() - - // To refresh when back to normal workflow from selection workflow - mReadOnlyToSave = ReadOnlyHelper.retrieveReadOnlyFromIntent(intent) - mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this) - - invalidateOptionsMenu() - - if (mTimeoutEnable) { - // End activity if database not loaded - if (!Database.getInstance().loaded) { - finish() - return - } - - // After the first creation - // or If simply swipe with another application - // If the time is out -> close the Activity - TimeoutHelper.checkTimeAndLockIfTimeout(this) - // If onCreate already record time - if (!mExitLock) - TimeoutHelper.recordTime(this) - } - - LOCKING_ACTIVITY_UI_VISIBLE = true - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable) - super.onSaveInstanceState(outState) - } - - override fun onPause() { - LOCKING_ACTIVITY_UI_VISIBLE = false - - mProgressDatabaseTaskProvider?.unregisterProgressTask() - - super.onPause() - - if (mTimeoutEnable) { - // If the time is out during our navigation in activity -> close the Activity - TimeoutHelper.checkTimeAndLockIfTimeout(this) - } - } - - override fun onDestroy() { - unregisterLockReceiver(mLockReceiver) - super.onDestroy() - } - - protected fun lockAndExit() { - sendBroadcast(Intent(LOCK_ACTION)) - } - - override fun onBackPressed() { - if (mTimeoutEnable) { - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this) { - super.onBackPressed() - } - } else { - super.onBackPressed() - } - } - - companion object { - - const val TAG = "LockingActivity" - - const val RESULT_EXIT_LOCK = 1450 - - const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY" - const val TIMEOUT_ENABLE_KEY_DEFAULT = true - - private var LOCKING_ACTIVITY_UI_VISIBLE = false - var LOCKING_ACTIVITY_UI_VISIBLE_DURING_LOCK: Boolean? = null - } -} - -/** - * To reset the app timeout when a view is focused or changed - */ -@SuppressLint("ClickableViewAccessibility") -fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context) { - setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - //Log.d(LockingActivity.TAG, "View touched, try to reset app timeout") - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context) - } - } - false - } - setOnFocusChangeListener { _, _ -> - //Log.d(LockingActivity.TAG, "View focused, try to reset app timeout") - TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context) - } - if (this is ViewGroup) { - for (i in 0..childCount) { - getChildAt(i)?.resetAppTimeoutWhenViewFocusedOrChanged(context) - } - } -} diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt index bc140450e..15f76e4f6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/stylish/StylishActivity.kt @@ -22,10 +22,13 @@ package com.kunzisoft.keepass.activities.stylish import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle -import androidx.annotation.StyleRes -import androidx.appcompat.app.AppCompatActivity +import android.os.Handler +import android.os.Looper import android.util.Log import android.view.WindowManager +import androidx.annotation.StyleRes +import androidx.appcompat.app.AppCompatActivity +import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_APPEARANCE_PREFERENCE_CHANGED /** * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from @@ -35,6 +38,7 @@ abstract class StylishActivity : AppCompatActivity() { @StyleRes private var themeId: Int = 0 + private var customStyle = true /* (non-Javadoc) Workaround for HTC Linkify issues * @see android.app.Activity#startActivity(android.content.Intent) @@ -52,10 +56,30 @@ abstract class StylishActivity : AppCompatActivity() { } } + open fun applyCustomStyle(): Boolean { + return true + } + + open fun finishActivityIfReloadRequested(): Boolean { + return false + } + + open fun reloadActivity() { + if (!finishActivityIfReloadRequested()) { + startActivity(intent) + } + finish() + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - this.themeId = Stylish.getThemeId(this) - setTheme(themeId) + + customStyle = applyCustomStyle() + if (customStyle) { + this.themeId = Stylish.getThemeId(this) + setTheme(themeId) + } // Several gingerbread devices have problems with FLAG_SECURE window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) @@ -63,9 +87,17 @@ abstract class StylishActivity : AppCompatActivity() { override fun onResume() { super.onResume() - if (Stylish.getThemeId(this) != this.themeId) { + + if ((customStyle && Stylish.getThemeId(this) != this.themeId) + || DATABASE_APPEARANCE_PREFERENCE_CHANGED) { + DATABASE_APPEARANCE_PREFERENCE_CHANGED = false Log.d(this.javaClass.name, "Theme change detected, restarting activity") - this.recreate() + recreateActivity() } } + + private fun recreateActivity() { + // To prevent KitKat bugs + Handler(Looper.getMainLooper()).post { recreate() } + } } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt index 42e216fc8..6a3dd1c4f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt @@ -26,13 +26,13 @@ import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.kunzisoft.keepass.R -import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.model.EntryInfo class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter() { private val inflater: LayoutInflater = LayoutInflater.from(context) - var entryHistoryList: MutableList = ArrayList() - var onItemClickListener: ((item: Entry, position: Int)->Unit)? = null + var entryHistoryList: MutableList = ArrayList() + var onItemClickListener: ((item: EntryInfo, position: Int)->Unit)? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EntryHistoryViewHolder { return EntryHistoryViewHolder(inflater.inflate(R.layout.item_list_entry_history, parent, false)) @@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.AdapterBoolean)? = null private var saveAliasListener: ((DatabaseFile)->Unit)? = null - private val listDatabaseFiles = ArrayList() + private var mDefaultDatabase: DatabaseFile? = null + private var mExpandedDatabaseFile: SuperDatabaseFile? = null + private var mPreviousExpandedDatabaseFile: SuperDatabaseFile? = null + + private val mListPosition = mutableListOf() + private val mSortedListDatabaseFiles = SortedList(SuperDatabaseFile::class.java, + object: SortedListAdapterCallback(this) { + override fun compare(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Int { + val indexItem1 = mListPosition.indexOf(item1) + val indexItem2 = mListPosition.indexOf(item2) + return if (indexItem1 == -1 && indexItem2 == -1) + -1 + else if (indexItem1 < indexItem2) + -1 + else if (indexItem1 > indexItem2) + 1 + else + 0 + } + + override fun areContentsTheSame(oldItem: SuperDatabaseFile, newItem: SuperDatabaseFile): Boolean { + val oldDatabaseFile = oldItem.databaseFile + val newDatabaseFile = newItem.databaseFile + return oldDatabaseFile.databaseUri == newDatabaseFile.databaseUri + && oldDatabaseFile.databaseDecodedPath == newDatabaseFile.databaseDecodedPath + && oldDatabaseFile.databaseAlias == newDatabaseFile.databaseAlias + && oldDatabaseFile.databaseFileExists == newDatabaseFile.databaseFileExists + && oldDatabaseFile.databaseLastModified == newDatabaseFile.databaseLastModified + && oldDatabaseFile.databaseSize == newDatabaseFile.databaseSize + && oldItem.default == newItem.default + } - private var mDefaultDatabaseFile: DatabaseFile? = null - private var mExpandedDatabaseFile: DatabaseFile? = null - private var mPreviousExpandedDatabaseFile: DatabaseFile? = null + override fun areItemsTheSame(item1: SuperDatabaseFile, item2: SuperDatabaseFile): Boolean { + return item1.databaseFile == item2.databaseFile + } + } + ) @ColorInt private val defaultColor: Int @@ -71,7 +105,8 @@ class FileDatabaseHistoryAdapter(context: Context) override fun onBindViewHolder(holder: FileDatabaseHistoryViewHolder, position: Int) { // Get info from position - val databaseFile = listDatabaseFiles[position] + val superDatabaseFile = mSortedListDatabaseFiles[position] + val databaseFile = superDatabaseFile.databaseFile // Click item to open file holder.fileContainer.setOnClickListener { @@ -80,7 +115,7 @@ class FileDatabaseHistoryAdapter(context: Context) // Default database holder.defaultFileButton.apply { - this.isChecked = mDefaultDatabaseFile == databaseFile + this.isChecked = superDatabaseFile.default setOnClickListener { defaultDatabaseListener?.invoke(if (isChecked) databaseFile else null) } @@ -115,7 +150,7 @@ class FileDatabaseHistoryAdapter(context: Context) } // Click on information - val isExpanded = databaseFile == mExpandedDatabaseFile + val isExpanded = superDatabaseFile == mExpandedDatabaseFile // Hides or shows info holder.fileExpandContainer.apply { if (isExpanded) { @@ -151,16 +186,16 @@ class FileDatabaseHistoryAdapter(context: Context) } if (isExpanded) { - mPreviousExpandedDatabaseFile = databaseFile + mPreviousExpandedDatabaseFile = superDatabaseFile } holder.fileInformationButton.apply { animate().rotation(if (isExpanded) 180F else 0F).start() setOnClickListener { - mExpandedDatabaseFile = if (isExpanded) null else databaseFile + mExpandedDatabaseFile = if (isExpanded) null else superDatabaseFile // Notify change - val previousExpandedPosition = listDatabaseFiles.indexOf(mPreviousExpandedDatabaseFile) + val previousExpandedPosition = mListPosition.indexOf(mPreviousExpandedDatabaseFile) notifyItemChanged(previousExpandedPosition) - val expandedPosition = listDatabaseFiles.indexOf(mExpandedDatabaseFile) + val expandedPosition = mListPosition.indexOf(mExpandedDatabaseFile) notifyItemChanged(expandedPosition) } } @@ -172,50 +207,67 @@ class FileDatabaseHistoryAdapter(context: Context) } override fun getItemCount(): Int { - return listDatabaseFiles.size + return mSortedListDatabaseFiles.size() } fun clearDatabaseFileHistoryList() { - listDatabaseFiles.clear() + mListPosition.clear() + mSortedListDatabaseFiles.clear() } fun addDatabaseFileHistory(fileDatabaseHistoryToAdd: DatabaseFile) { - listDatabaseFiles.add(0, fileDatabaseHistoryToAdd) - notifyItemInserted(0) + val superToAdd = SuperDatabaseFile(fileDatabaseHistoryToAdd) + mListPosition.add(0, superToAdd) + mSortedListDatabaseFiles.add(superToAdd) } fun updateDatabaseFileHistory(fileDatabaseHistoryToUpdate: DatabaseFile) { - val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToUpdate) - if (listDatabaseFiles.remove(fileDatabaseHistoryToUpdate)) { - listDatabaseFiles.add(index, fileDatabaseHistoryToUpdate) - notifyItemChanged(index) + val superToUpdate = SuperDatabaseFile(fileDatabaseHistoryToUpdate) + val index = mListPosition.indexOf(superToUpdate) + if (mListPosition.remove(superToUpdate)) { + mListPosition.add(index, superToUpdate) } + mSortedListDatabaseFiles.updateItemAt(index, superToUpdate) } fun deleteDatabaseFileHistory(fileDatabaseHistoryToDelete: DatabaseFile) { - val index = listDatabaseFiles.indexOf(fileDatabaseHistoryToDelete) - if (listDatabaseFiles.remove(fileDatabaseHistoryToDelete)) { - notifyItemRemoved(index) - } + val superToDelete = SuperDatabaseFile(fileDatabaseHistoryToDelete) + val index = mListPosition.indexOf(superToDelete) + mListPosition.remove(superToDelete) + mSortedListDatabaseFiles.removeItemAt(index) } fun replaceAllDatabaseFileHistoryList(listFileDatabaseHistoryToAdd: List) { - if (listDatabaseFiles.isEmpty()) { - listFileDatabaseHistoryToAdd.forEach { - listDatabaseFiles.add(it) - notifyItemInserted(listDatabaseFiles.size) - } - } else { - listDatabaseFiles.clear() - listDatabaseFiles.addAll(listFileDatabaseHistoryToAdd) - notifyDataSetChanged() + val superMapToReplace = listFileDatabaseHistoryToAdd.map { + SuperDatabaseFile(it) } + mListPosition.clear() + mListPosition.addAll(superMapToReplace) + mSortedListDatabaseFiles.replaceAll(superMapToReplace) } fun setDefaultDatabase(databaseUri: Uri?) { - val defaultDatabaseFile = listDatabaseFiles.firstOrNull { it.databaseUri == databaseUri } - mDefaultDatabaseFile = defaultDatabaseFile - notifyDataSetChanged() + // Remove default from last item + val oldDefaultDatabasePosition = mListPosition.indexOfFirst { + it.default + } + if (oldDefaultDatabasePosition >= 0) { + val oldDefaultDatabase = mListPosition[oldDefaultDatabasePosition].apply { + default = false + } + mSortedListDatabaseFiles.updateItemAt(oldDefaultDatabasePosition, oldDefaultDatabase) + } + // Add default to new item + val newDefaultDatabaseFilePosition = mListPosition.indexOfFirst { + it.databaseFile.databaseUri == databaseUri + } + if (newDefaultDatabaseFilePosition >= 0) { + val newDefaultDatabase = mListPosition[newDefaultDatabaseFilePosition].apply { + default = true + } + mDefaultDatabase = newDefaultDatabase.databaseFile + mSortedListDatabaseFiles.updateItemAt(newDefaultDatabaseFilePosition, newDefaultDatabase) + } } fun setOnDefaultDatabaseListener(listener: ((DatabaseFile?) -> Unit)?) { @@ -234,6 +286,30 @@ class FileDatabaseHistoryAdapter(context: Context) this.saveAliasListener = listener } + private inner class SuperDatabaseFile( + var databaseFile: DatabaseFile, + var default: Boolean = false + ) { + + init { + if (mDefaultDatabase == databaseFile) + this.default = true + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SuperDatabaseFile) return false + + if (databaseFile != other.databaseFile) return false + + return true + } + + override fun hashCode(): Int { + return databaseFile.hashCode() + } + } + class FileDatabaseHistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var fileContainer: ViewGroup = itemView.findViewById(R.id.file_container_basic_info) diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt index 4487bd776..82b03bd47 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodeAdapter.kt @@ -26,7 +26,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.ProgressBar import android.widget.TextView +import android.widget.Toast import androidx.annotation.ColorInt import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView @@ -40,7 +42,11 @@ import com.kunzisoft.keepass.database.element.SortNodeEnum import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.NodeVersionedInterface import com.kunzisoft.keepass.database.element.node.Type +import com.kunzisoft.keepass.database.element.template.TemplateField +import com.kunzisoft.keepass.otp.OtpElement +import com.kunzisoft.keepass.otp.OtpType import com.kunzisoft.keepass.settings.PreferencesUtil +import com.kunzisoft.keepass.timeout.ClipboardHelper import com.kunzisoft.keepass.view.setTextSize import com.kunzisoft.keepass.view.strikeOut import java.util.* @@ -49,7 +55,8 @@ import java.util.* * Create node list adapter with contextMenu or not * @param context Context to use */ -class NodeAdapter (private val context: Context) +class NodeAdapter (private val context: Context, + private val database: Database) : RecyclerView.Adapter() { private var mNodeComparator: Comparator>? = null @@ -67,12 +74,13 @@ class NodeAdapter (private val context: Context) private var mShowUserNames: Boolean = true private var mShowNumberEntries: Boolean = true + private var mShowOTP: Boolean = false + private var mShowUUID: Boolean = false private var mEntryFilters = arrayOf() private var mActionNodesList = LinkedList() private var mNodeClickCallback: NodeClickCallback? = null - - private val mDatabase: Database + private var mClipboardHelper = ClipboardHelper(context) @ColorInt private val mContentSelectionColor: Int @@ -96,9 +104,6 @@ class NodeAdapter (private val context: Context) this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) - // Database - this.mDatabase = Database.getInstance() - // Color of content selection this.mContentSelectionColor = ContextCompat.getColor(context, R.color.white) // Retrieve the color to tint the icon @@ -111,7 +116,7 @@ class NodeAdapter (private val context: Context) taTextColor.recycle() } - fun assignPreferences() { + private fun assignPreferences() { this.mPrefSizeMultiplier = PreferencesUtil.getListTextSize(context) notifyChangeSort( @@ -125,6 +130,8 @@ class NodeAdapter (private val context: Context) this.mShowUserNames = PreferencesUtil.showUsernamesListEntries(context) this.mShowNumberEntries = PreferencesUtil.showNumberEntries(context) + this.mShowOTP = PreferencesUtil.showOTPToken(context) + this.mShowUUID = PreferencesUtil.showUUID(context) this.mEntryFilters = Group.ChildFilter.getDefaults(context) @@ -146,9 +153,21 @@ class NodeAdapter (private val context: Context) } override fun areContentsTheSame(oldItem: Node, newItem: Node): Boolean { - return oldItem.type == newItem.type + var typeContentTheSame = true + if (oldItem is Entry && newItem is Entry) { + typeContentTheSame = oldItem.getVisualTitle() == newItem.getVisualTitle() + && oldItem.username == newItem.username + && oldItem.getOtpElement() == newItem.getOtpElement() + && oldItem.containsAttachment() == newItem.containsAttachment() + } else if (oldItem is Group && newItem is Group) { + typeContentTheSame = oldItem.numberOfChildEntries == newItem.numberOfChildEntries + } + return typeContentTheSame + && oldItem.nodeId == newItem.nodeId + && oldItem.type == newItem.type && oldItem.title == newItem.title && oldItem.icon == newItem.icon + && oldItem.isCurrentlyExpires == newItem.isCurrentlyExpires } override fun areItemsTheSame(item1: Node, item2: Node): Boolean { @@ -241,6 +260,10 @@ class NodeAdapter (private val context: Context) mNodeSortedList.endBatchedUpdates() } + fun indexOf(node: Node): Int { + return mNodeSortedList.indexOf(node) + } + fun notifyNodeChanged(node: Node) { notifyItemChanged(mNodeSortedList.indexOf(node)) } @@ -266,7 +289,7 @@ class NodeAdapter (private val context: Context) */ fun notifyChangeSort(sortNodeEnum: SortNodeEnum, sortNodeParameters: SortNodeEnum.SortNodeParameters) { - this.mNodeComparator = sortNodeEnum.getNodeComparator(sortNodeParameters) + this.mNodeComparator = sortNodeEnum.getNodeComparator(database, sortNodeParameters) } override fun getItemViewType(position: Int): Int { @@ -303,7 +326,7 @@ class NodeAdapter (private val context: Context) } holder.imageIdentifier?.setColorFilter(iconColor) holder.icon.apply { - mDatabase.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) + database.iconDrawableFactory.assignDatabaseIcon(this, subNode.icon, iconColor) // Relative size of the icon layoutParams?.apply { height = (mIconDefaultDimension * mPrefSizeMultiplier).toInt() @@ -323,11 +346,16 @@ class NodeAdapter (private val context: Context) strikeOut(subNode.isCurrentlyExpires) visibility = View.GONE } + // Add meta text to show UUID + holder.meta.apply { + text = subNode.nodeId.toString() + visibility = if (mShowUUID) View.VISIBLE else View.GONE + } // Specific elements for entry if (subNode.type == Type.ENTRY) { val entry = subNode as Entry - mDatabase.startManageEntry(entry) + database.startManageEntry(entry) holder.text.text = entry.getVisualTitle() holder.subText.apply { @@ -339,10 +367,29 @@ class NodeAdapter (private val context: Context) } } + val otpElement = entry.getOtpElement() + holder.otpContainer?.removeCallbacks(holder.otpRunnable) + if (otpElement != null + && mShowOTP + && otpElement.token.isNotEmpty()) { + + // Execute runnable to show progress + holder.otpRunnable.action = { + populateOtpView(holder, otpElement) + } + if (otpElement.type == OtpType.TOTP) { + holder.otpRunnable.postDelayed() + } + populateOtpView(holder, otpElement) + + holder.otpContainer?.visibility = View.VISIBLE + } else { + holder.otpContainer?.visibility = View.GONE + } holder.attachmentIcon?.visibility = if (entry.containsAttachment()) View.VISIBLE else View.GONE - mDatabase.stopManageEntry(entry) + database.stopManageEntry(entry) } // Add number of entries in groups @@ -350,7 +397,7 @@ class NodeAdapter (private val context: Context) if (mShowNumberEntries) { holder.numberChildren?.apply { text = (subNode as Group) - .getNumberOfChildEntries(mEntryFilters) + .numberOfChildEntries .toString() setTextSize(mTextSizeUnit, mNumberChildrenTextDefaultDimension, mPrefSizeMultiplier) visibility = View.VISIBLE @@ -362,13 +409,56 @@ class NodeAdapter (private val context: Context) // Assign click holder.container.setOnClickListener { - mNodeClickCallback?.onNodeClick(subNode) + mNodeClickCallback?.onNodeClick(database, subNode) } holder.container.setOnLongClickListener { - mNodeClickCallback?.onNodeLongClick(subNode) ?: false + mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false } } - + + private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) { + when (otpElement?.type) { + OtpType.HOTP -> { + holder?.otpProgress?.apply { + max = 100 + progress = 100 + } + } + OtpType.TOTP -> { + holder?.otpProgress?.apply { + max = otpElement.period + progress = otpElement.secondsRemaining + } + } + } + holder?.otpToken?.text = otpElement?.token + holder?.otpContainer?.setOnClickListener { + otpElement?.token?.let { token -> + Toast.makeText( + context, + context.getString(R.string.copy_field, + TemplateField.getLocalizedName(context, TemplateField.LABEL_TOKEN)), + Toast.LENGTH_LONG + ).show() + mClipboardHelper.copyToClipboard(token) + } + } + } + + class OtpRunnable(val view: View?): Runnable { + + var action: (() -> Unit)? = null + + override fun run() { + action?.invoke() + postDelayed() + } + + fun postDelayed() { + view?.postDelayed(this, 1000) + } + } + override fun getItemCount(): Int { return mNodeSortedList.size() } @@ -384,8 +474,8 @@ class NodeAdapter (private val context: Context) * Callback listener to redefine to do an action when a node is click */ interface NodeClickCallback { - fun onNodeClick(node: Node) - fun onNodeLongClick(node: Node): Boolean + fun onNodeClick(database: Database, node: Node) + fun onNodeLongClick(database: Database, node: Node): Boolean } class NodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { @@ -394,6 +484,11 @@ class NodeAdapter (private val context: Context) var icon: ImageView = itemView.findViewById(R.id.node_icon) var text: TextView = itemView.findViewById(R.id.node_text) var subText: TextView = itemView.findViewById(R.id.node_subtext) + var meta: TextView = itemView.findViewById(R.id.node_meta) + var otpContainer: ViewGroup? = itemView.findViewById(R.id.node_otp_container) + var otpProgress: ProgressBar? = itemView.findViewById(R.id.node_otp_progress) + var otpToken: TextView? = itemView.findViewById(R.id.node_otp_token) + var otpRunnable: OtpRunnable = OtpRunnable(otpContainer) var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers) var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon) } diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt new file mode 100644 index 000000000..53698f3a8 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/TemplatesSelectorAdapter.kt @@ -0,0 +1,70 @@ +package com.kunzisoft.keepass.adapters + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.TextView +import com.kunzisoft.keepass.R +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.template.Template +import com.kunzisoft.keepass.database.element.template.TemplateField +import com.kunzisoft.keepass.icons.IconDrawableFactory + + +class TemplatesSelectorAdapter(private val context: Context, + private val iconDrawableFactory: IconDrawableFactory?, + private var templates: List