diff --git a/CHANGELOG b/CHANGELOG index b792d89bc..7c92ef631 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,10 @@ 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 - * Small changes #1035 #1043 #942 + * Improvements #1035 #1043 #942 #1021 #1027 KeePassDX(2.10.5) * Increase the saving speed of database #1028 diff --git a/app/src/main/java/com/igreenwood/loupe/Loupe.kt b/app/src/main/java/com/igreenwood/loupe/Loupe.kt index e94a84411..95a1e453f 100644 --- a/app/src/main/java/com/igreenwood/loupe/Loupe.kt +++ b/app/src/main/java/com/igreenwood/loupe/Loupe.kt @@ -30,6 +30,7 @@ package com.igreenwood.loupe import android.animation.Animator import android.animation.ObjectAnimator import android.animation.ValueAnimator +import android.annotation.SuppressLint import android.graphics.Matrix import android.graphics.PointF import android.graphics.Rect @@ -108,6 +109,8 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener, var viewDragFriction = DEFAULT_VIEW_DRAG_FRICTION // drag distance threshold in dp for swipe to dismiss var dragDismissDistanceInDp = DEFAULT_DRAG_DISMISS_DISTANCE_IN_DP + // on view touched + var onViewTouchedListener: View.OnTouchListener? = null // on view translate listener var onViewTranslateListener: OnViewTranslateListener? = null // on scale changed @@ -272,7 +275,10 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener, private var imageViewRef: WeakReference = 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/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt index ed096d000..552fe5f51 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -157,11 +157,6 @@ class EntryActivity : DatabaseLockActivity() { invalidateOptionsMenu() } - mEntryViewModel.url.observe(this) { url -> - this.mUrl = url - invalidateOptionsMenu() - } - mEntryViewModel.entryInfo.observe(this) { entryInfo -> // Manage entry copy to start notification if allowed (at the first start) if (savedInstanceState == null) { @@ -184,6 +179,8 @@ class EntryActivity : DatabaseLockActivity() { collapsingToolbarLayout?.title = entryTitle toolbar?.title = entryTitle + mUrl = entryInfo.url + // Refresh Menu invalidateOptionsMenu() 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 d747eba28..707d2ffbb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt @@ -143,13 +143,13 @@ class EntryEditActivity : DatabaseLockActivity(), // Entry is retrieve, it's an entry to update intent.getParcelableExtra>(KEY_ENTRY)?.let { entryToUpdate -> - intent.removeExtra(KEY_ENTRY) + //intent.removeExtra(KEY_ENTRY) mEntryId = entryToUpdate } // Parent is retrieve, it's a new entry to create intent.getParcelableExtra>(KEY_PARENT)?.let { parent -> - intent.removeExtra(KEY_PARENT) + //intent.removeExtra(KEY_PARENT) mParentId = parent } @@ -166,7 +166,37 @@ class EntryEditActivity : DatabaseLockActivity(), // Save button validateButton?.setOnClickListener { saveEntry() } - mEntryEditViewModel.entryInfo.observe(this) { + mEntryEditViewModel.templatesEntry.observe(this) { templatesEntry -> + // Change template dynamically + templatesEntry?.templates?.let { templates -> + val defaultTemplate = templatesEntry.defaultTemplate + templateSelectorSpinner?.apply { + // Build template selector + if (templates.isNotEmpty()) { + adapter = TemplatesSelectorAdapter( + this@EntryEditActivity, + mIconDrawableFactory, + templates + ) + setSelection(templates.indexOf(defaultTemplate)) + 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() } @@ -238,27 +268,6 @@ class EntryEditActivity : DatabaseLockActivity(), mAttachmentFileBinderManager?.removeBinaryAttachment(it) } - // Change template dynamically - mEntryEditViewModel.templates.observe(this) { templatesLoaded -> - val templates = templatesLoaded.templates - val defaultTemplate = templatesLoaded.defaultTemplate - templateSelectorSpinner?.apply { - // Build template selector - if (templates.isNotEmpty()) { - adapter = TemplatesSelectorAdapter(this@EntryEditActivity, mIconDrawableFactory, templates) - setSelection(templates.indexOf(defaultTemplate)) - 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 - } - } - } - // Build new entry from the entry info retrieved mEntryEditViewModel.onEntrySaved.observe(this) { entrySave -> // Open a progress dialog and save entry 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 cb9bedae5..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 @@ -43,6 +44,7 @@ class ImageViewerActivity : DatabaseLockActivity() { private lateinit var imageView: ImageView private lateinit var progressView: View + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -52,12 +54,21 @@ class ImageViewerActivity : DatabaseLockActivity() { setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) + toolbar.setOnTouchListener { _, _ -> + resetAppTimeout() + false + } imageContainerView = findViewById(R.id.image_viewer_container) imageView = findViewById(R.id.image_viewer_image) progressView = findViewById(R.id.image_viewer_progress) Loupe.create(imageView, imageContainerView!!) { + onViewTouchedListener = View.OnTouchListener { _, _ -> + // to reset timeout when Loupe image view touched + resetAppTimeout() + false + } onViewTranslateListener = object : Loupe.OnViewTranslateListener { override fun onStart(view: ImageView) { @@ -81,7 +92,8 @@ class ImageViewerActivity : DatabaseLockActivity() { } override fun viewToInvalidateTimeout(): View? { - return imageContainerView + // Null to manually manage events + return null } override fun finishActivityIfReloadRequested(): Boolean { 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 index 6b655bc0b..1cacb3d3c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt @@ -4,9 +4,10 @@ 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.resetAppTimeoutWhenViewFocusedOrChanged +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 { @@ -19,7 +20,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { mDatabaseViewModel.database.observe(this) { database -> this.mDatabase = database - resetAppTimeoutWhenViewFocusedOrChanged() + resetAppTimeoutOnTouchOrFocus() onDatabaseRetrieved(database) } @@ -31,7 +32,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - resetAppTimeoutWhenViewFocusedOrChanged() + resetAppTimeoutOnTouchOrFocus() } override fun onDatabaseRetrieved(database: Database?) { @@ -46,9 +47,25 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval { // Can be overridden by a subclass } - private fun resetAppTimeoutWhenViewFocusedOrChanged() { + fun resetAppTimeout() { context?.let { - dialog?.window?.decorView?.resetAppTimeoutWhenViewFocusedOrChanged(it, mDatabase?.loaded) + 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 0581932ac..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 @@ -4,8 +4,10 @@ import android.app.DatePickerDialog import android.app.Dialog import android.content.Context import android.os.Bundle +import androidx.fragment.app.DialogFragment -class DatePickerFragment : DatabaseDialogFragment() { +// Not as DatabaseDialogFragment because crash on KitKat +class DatePickerFragment : DialogFragment() { private var mDefaultYear: Int = 2000 private var mDefaultMonth: Int = 1 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/SetOTPDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/SetOTPDialogFragment.kt index 25d4de6bb..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 @@ -79,11 +79,15 @@ class SetOTPDialogFragment : DatabaseDialogFragment() { 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 @@ -94,6 +98,10 @@ class SetOTPDialogFragment : DatabaseDialogFragment() { 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 @@ -224,8 +232,11 @@ class SetOTPDialogFragment : DatabaseDialogFragment() { 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/TimePickerFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/TimePickerFragment.kt index 698ccd319..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 @@ -6,8 +6,10 @@ import android.app.TimePickerDialog import android.content.Context import android.os.Bundle import android.text.format.DateFormat +import androidx.fragment.app.DialogFragment -class TimePickerFragment : DatabaseDialogFragment() { +// Not as DatabaseDialogFragment because crash on KitKat +class TimePickerFragment : DialogFragment() { private var defaultHour: Int = 0 private var defaultMinute: 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 index ea94ee0e4..671f5fff7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/DatabaseFragment.kt @@ -4,7 +4,7 @@ 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.resetAppTimeoutWhenViewFocusedOrChanged +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 @@ -33,7 +33,7 @@ abstract class DatabaseFragment : StylishFragment(), DatabaseRetrieval { protected fun resetAppTimeoutWhenViewFocusedOrChanged(view: View?) { context?.let { - view?.resetAppTimeoutWhenViewFocusedOrChanged(it, mDatabase?.loaded) + view?.resetAppTimeoutWhenViewTouchedOrFocused(it, mDatabase?.loaded) } } 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 c854b535b..1dc74737d 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 @@ -118,10 +118,11 @@ class EntryEditFragment: DatabaseFragment() { templateView.setTemplate(template) } - mEntryEditViewModel.entryInfo.observe(viewLifecycleOwner) { entryInfo -> + mEntryEditViewModel.templatesEntry.observe(viewLifecycleOwner) { templateEntry -> + templateView.setTemplate(templateEntry.defaultTemplate) // Load entry info only the first time to keep change locally if (savedInstanceState == null) { - assignEntryInfo(entryInfo) + assignEntryInfo(templateEntry.entryInfo) } // To prevent flickering rootView.showByFading() diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt index 8dac97f0d..d3feb1986 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt @@ -219,6 +219,8 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen activity?.intent?.let { specialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(it) } + + rebuildList() } override fun onPause() { 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 index b4c95623b..626773cb1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt @@ -169,7 +169,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), // Focus view to reinitialize timeout, // view is not necessary loaded so retry later in resume viewToInvalidateTimeout() - ?.resetAppTimeoutWhenViewFocusedOrChanged(this, database?.loaded) + ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database?.loaded) database?.let { // check timeout @@ -383,7 +383,7 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), // Invalidate timeout by touch mDatabase?.let { database -> viewToInvalidateTimeout() - ?.resetAppTimeoutWhenViewFocusedOrChanged(this, database.loaded) + ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded) } invalidateOptionsMenu() @@ -422,6 +422,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), sendBroadcast(Intent(LOCK_ACTION)) } + fun resetAppTimeout() { + TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this, + mDatabase?.loaded ?: false) + } + override fun onBackPressed() { if (mTimeoutEnable) { TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this, @@ -451,12 +456,12 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(), * To reset the app timeout when a view is focused or changed */ @SuppressLint("ClickableViewAccessibility") -fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context, databaseLoaded: Boolean?) { - // Log.d(LockingActivity.TAG, "View prepared to reset app timeout") +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(LockingActivity.TAG, "View touched, try to reset app timeout") + // Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout") TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(context, databaseLoaded ?: false) } @@ -464,13 +469,13 @@ fun View.resetAppTimeoutWhenViewFocusedOrChanged(context: Context, databaseLoade false } setOnFocusChangeListener { _, _ -> - // Log.d(LockingActivity.TAG, "View focused, try to reset app timeout") + // 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)?.resetAppTimeoutWhenViewFocusedOrChanged(context, databaseLoaded) + getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded) } } } 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 2075eae9b..6a3dd1c4f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/EntryHistoryAdapter.kt @@ -44,7 +44,6 @@ class EntryHistoryAdapter(val context: Context) : RecyclerView.Adapter() @@ -122,6 +126,7 @@ 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) @@ -148,6 +153,7 @@ class NodeAdapter (private val context: Context, 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 @@ -357,6 +363,25 @@ 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 @@ -386,7 +411,41 @@ class NodeAdapter (private val context: Context, mNodeClickCallback?.onNodeLongClick(database, subNode) ?: false } } - + + private fun populateOtpView(holder: NodeViewHolder?, otpElement: OtpElement?) { + when (otpElement?.type) { + OtpType.HOTP -> { + holder?.otpCounter?.text = otpElement.counter.toString() + holder?.otpProgress?.apply { + max = 100 + progress = 100 + } + } + OtpType.TOTP -> { + holder?.otpCounter?.text = otpElement.secondsRemaining.toString() + holder?.otpProgress?.apply { + max = otpElement.period + progress = otpElement.secondsRemaining + } + } + } + holder?.otpToken?.text = otpElement?.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() } @@ -413,6 +472,11 @@ class NodeAdapter (private val context: Context, 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 otpCounter: TextView? = itemView.findViewById(R.id.node_otp_counter) + 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/model/OtpModel.kt b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt index 106375784..ecfff03f9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/model/OtpModel.kt @@ -56,7 +56,7 @@ class OtpModel() : Parcelable { if (this === other) return true if (javaClass != other?.javaClass) return false - other as OtpElement + other as OtpModel if (type != other.type) return false // Token type is important only if it's a TOTP diff --git a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt index 5b34eabc9..d07ace49f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt +++ b/app/src/main/java/com/kunzisoft/keepass/otp/OtpElement.kt @@ -185,6 +185,19 @@ data class OtpElement(var otpModel: OtpModel = OtpModel()) { return secondsRemaining == otpModel.period } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is OtpElement) return false + + if (otpModel != other.otpModel) return false + + return true + } + + override fun hashCode(): Int { + return otpModel.hashCode() + } + companion object { const val MIN_HOTP_COUNTER = 0 const val MAX_HOTP_COUNTER = Long.MAX_VALUE diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt index 3e85b4d6e..bb7a23cd5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedAppSettingsFragment.kt @@ -454,7 +454,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { } override fun onPreferenceTreeClick(preference: Preference?): Boolean { - // To reload group when appearence settings are modified + // To reload group when appearance settings are modified when (preference?.key) { getString(R.string.setting_style_key), getString(R.string.setting_style_brightness_key), @@ -464,6 +464,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() { getString(R.string.list_size_key), getString(R.string.monospace_font_fields_enable_key), getString(R.string.hide_expired_entries_key), + getString(R.string.show_otp_token_key), getString(R.string.show_uuid_key), getString(R.string.enable_education_screens_key), getString(R.string.reset_education_screens_key) -> { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt index d72b36ebb..dfbdbb0a1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/NestedDatabaseSettingsFragment.kt @@ -32,7 +32,7 @@ import com.kunzisoft.androidclearchroma.ChromaUtil import com.kunzisoft.keepass.R import com.kunzisoft.keepass.activities.legacy.DatabaseRetrieval import com.kunzisoft.keepass.activities.dialogs.AssignMasterKeyDialogFragment -import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewFocusedOrChanged +import com.kunzisoft.keepass.activities.legacy.resetAppTimeoutWhenViewTouchedOrFocused import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.element.Database @@ -74,7 +74,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev mDatabaseViewModel.database.observe(viewLifecycleOwner) { database -> mDatabase = database - view.resetAppTimeoutWhenViewFocusedOrChanged(requireContext(), database?.loaded) + view.resetAppTimeoutWhenViewTouchedOrFocused(requireContext(), database?.loaded) onDatabaseRetrieved(database) } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt index 5661d1dbd..c2059d234 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/PreferencesUtil.kt @@ -128,6 +128,12 @@ object PreferencesUtil { context.resources.getBoolean(R.bool.hide_expired_entries_default)) } + fun showOTPToken(context: Context): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getBoolean(context.getString(R.string.show_otp_token_key), + context.resources.getBoolean(R.bool.show_otp_token_default)) + } + fun showUUID(context: Context): Boolean { val prefs = PreferenceManager.getDefaultSharedPreferences(context) return prefs.getBoolean(context.getString(R.string.show_uuid_key), @@ -644,6 +650,7 @@ object PreferencesUtil { context.getString(R.string.list_size_key) -> editor.putString(name, value) context.getString(R.string.monospace_font_fields_enable_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.hide_expired_entries_key) -> editor.putBoolean(name, value.toBoolean()) + context.getString(R.string.show_otp_token_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.show_uuid_key) -> editor.putBoolean(name, value.toBoolean()) context.getString(R.string.enable_education_screens_key) -> editor.putBoolean(name, value.toBoolean()) diff --git a/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt b/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt index 7c7ac123f..14f550819 100644 --- a/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/timeout/ClipboardHelper.kt @@ -20,7 +20,6 @@ package com.kunzisoft.keepass.timeout import android.app.Activity -import android.app.AlertDialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -30,6 +29,7 @@ import android.text.method.LinkMovementMethod import android.text.util.Linkify import android.widget.TextView import android.widget.Toast +import androidx.appcompat.app.AlertDialog import com.kunzisoft.keepass.R import com.kunzisoft.keepass.database.exception.ClipboardException import com.kunzisoft.keepass.settings.PreferencesUtil diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt index 4225eb05a..ac225371e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/utils/UriUtil.kt @@ -239,8 +239,23 @@ object UriUtil { } fun openExternalApp(context: Context, packageName: String) { + var launchIntent: Intent? = null try { - context.startActivity(context.applicationContext.packageManager.getLaunchIntentForPackage(packageName)) + launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } catch (ignored: Exception) { + } + try { + if (launchIntent == null) { + context.startActivity( + Intent(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName")) + ) + } else { + context.startActivity(launchIntent) + } } catch (e: Exception) { Log.e(TAG, "App cannot be open", e) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt index 9a214e859..1bcef3cd7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt @@ -39,7 +39,7 @@ abstract class TemplateAbstractView< private var mTemplate: Template? = null protected var mEntryInfo: EntryInfo? = null - protected var mCustomFieldIds = mutableListOf() + private var mViewFields = mutableListOf() protected var mFontInVisibility: Boolean = PreferencesUtil.fieldFontIsInVisibility(context) protected var mHideProtectedValue: Boolean = PreferencesUtil.hideProtectedValue(context) @@ -48,7 +48,7 @@ abstract class TemplateAbstractView< protected var entryIconView: ImageView private var titleContainerView: ViewGroup protected var templateContainerView: ViewGroup - protected var customFieldsContainerView: SectionView + private var customFieldsContainerView: SectionView private var notReferencedFieldsContainerView: SectionView init { @@ -95,7 +95,7 @@ abstract class TemplateAbstractView< } } - fun buildTemplate() { + private fun buildTemplate() { // Retrieve preferences mHideProtectedValue = PreferencesUtil.hideProtectedValue(context) @@ -104,7 +104,7 @@ abstract class TemplateAbstractView< templateContainerView.removeAllViews() customFieldsContainerView.removeAllViews() notReferencedFieldsContainerView.removeAllViews() - mCustomFieldIds.clear() + mViewFields.clear() mTemplate?.let { template -> @@ -200,7 +200,7 @@ abstract class TemplateAbstractView< TemplateAttributeType.TEXT, field.protectedValue.isProtected, TemplateAttributeOption().apply { - setNumberLines(3) + setNumberLines(20) }, TemplateAttributeAction.CUSTOM_EDITION ).apply { @@ -231,13 +231,13 @@ abstract class TemplateAbstractView< // Add new custom view id to the custom field list if (fieldTag == FIELD_CUSTOM_TAG) { - val indexOldItem = indexCustomFieldIdByName(field.name) + val indexOldItem = getIndexViewFieldByName(field.name) if (indexOldItem >= 0) - mCustomFieldIds.removeAt(indexOldItem) + mViewFields.removeAt(indexOldItem) if (itemView?.id != null) { - mCustomFieldIds.add( - FieldId( - itemView.id, + mViewFields.add( + ViewField( + itemView, field ) ) @@ -320,7 +320,7 @@ abstract class TemplateAbstractView< /** * Return empty custom fields */ - protected open fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { + protected open fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { mEntryInfo?.let { entryInfo -> populateEntryFieldView(FIELD_TITLE_TAG, @@ -350,15 +350,14 @@ abstract class TemplateAbstractView< showEmptyFields) customFieldsContainerView.removeAllViews() - val emptyCustomFields = mutableListOf().also { it.addAll(mCustomFieldIds) } + val emptyCustomFields = mutableListOf().also { it.addAll(mViewFields) } entryInfo.customFields.forEach { customField -> - val indexFieldViewId = indexCustomFieldIdByName(customField.name) + val indexFieldViewId = getIndexViewFieldByName(customField.name) if (indexFieldViewId >= 0) { // Template contains the custom view - val customFieldId = mCustomFieldIds[indexFieldViewId] - emptyCustomFields.remove(customFieldId) - templateContainerView.findViewById(customFieldId.viewId) - ?.let { customView -> + val viewField = mViewFields[indexFieldViewId] + emptyCustomFields.remove(viewField) + viewField.view.let { customView -> if (customView is GenericTextFieldView) { customView.value = customField.protectedValue.stringValue customView.applyFontVisibility(mFontInVisibility) @@ -459,22 +458,22 @@ abstract class TemplateAbstractView< * ------------- */ - protected data class FieldId(var viewId: Int, var field: Field) + protected data class ViewField(var view: View, var field: Field) private fun isStandardFieldName(name: String): Boolean { return TemplateField.isStandardFieldName(name) } - protected fun customFieldIdByName(name: String): FieldId? { - return mCustomFieldIds.find { it.field.name.equals(name, true) } + protected fun getViewFieldByName(name: String): ViewField? { + return mViewFields.find { it.field.name.equals(name, true) } } - protected fun indexCustomFieldIdByName(name: String): Int { - return mCustomFieldIds.indexOfFirst { it.field.name.equals(name, true) } + private fun getIndexViewFieldByName(name: String): Int { + return mViewFields.indexOfFirst { it.field.name.equals(name, true) } } private fun retrieveCustomFieldsFromView(templateFieldNotEmpty: Boolean = false) { - mEntryInfo?.customFields = mCustomFieldIds.mapNotNull { + mEntryInfo?.customFields = mViewFields.mapNotNull { getCustomField(it.field.name, templateFieldNotEmpty) }.toMutableList() } @@ -484,10 +483,9 @@ abstract class TemplateAbstractView< ?: Field(fieldName, ProtectedString(false)) } - protected fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? { - customFieldIdByName(fieldName)?.let { fieldId -> - val editView: View? = templateContainerView.findViewById(fieldId.viewId) - ?: customFieldsContainerView.findViewById(fieldId.viewId) + private fun getCustomField(fieldName: String, templateFieldNotEmpty: Boolean): Field? { + getViewFieldByName(fieldName)?.let { fieldId -> + val editView: View? = fieldId.view if (editView is GenericFieldView) { // Do not return field with a default value val defaultViewValue = if (editView.value == editView.default) "" else editView.value @@ -514,20 +512,22 @@ abstract class TemplateAbstractView< return if (!isStandardFieldName(customField.name)) { customFieldsContainerView.visibility = View.VISIBLE - if (indexCustomFieldIdByName(customField.name) >= 0) { - replaceCustomField(customField, customField, focus) + if (getIndexViewFieldByName(customField.name) >= 0) { + // Update a custom field with a new value, + // new field name must be the same as old field name + replaceCustomField(customField, customField, false, focus) } else { val newCustomView = buildViewForCustomField(customField) newCustomView?.let { customFieldsContainerView.addView(newCustomView) - val fieldId = FieldId( - newCustomView.id, + val fieldId = ViewField( + newCustomView, customField ) - val indexOldItem = indexCustomFieldIdByName(fieldId.field.name) + val indexOldItem = getIndexViewFieldByName(fieldId.field.name) if (indexOldItem >= 0) - mCustomFieldIds.removeAt(indexOldItem) - mCustomFieldIds.add(indexOldItem, fieldId) + mViewFields.removeAt(indexOldItem) + mViewFields.add(indexOldItem, fieldId) if (focus) newCustomView.requestFocus() } @@ -544,58 +544,63 @@ abstract class TemplateAbstractView< return put } - /** - * Update a custom field and keep the old value - */ - private fun replaceCustomField(oldField: Field, newField: Field, focus: Boolean): Boolean { + private fun replaceCustomField(oldField: Field, + newField: Field, + keepOldValue: Boolean, + focus: Boolean): Boolean { if (!isStandardFieldName(newField.name)) { - customFieldIdByName(oldField.name)?.viewId?.let { viewId -> - customFieldsContainerView.findViewById(viewId)?.let { viewToReplace -> - val oldValue = getCustomField(oldField.name).protectedValue.toString() + getViewFieldByName(oldField.name)?.view?.let { viewToReplace -> + val oldValue = getCustomField(oldField.name).protectedValue.toString() - val parentGroup = viewToReplace.parent as ViewGroup - val indexInParent = parentGroup.indexOfChild(viewToReplace) - parentGroup.removeView(viewToReplace) + val parentGroup = viewToReplace.parent as ViewGroup + val indexInParent = parentGroup.indexOfChild(viewToReplace) + parentGroup.removeView(viewToReplace) - val newCustomFieldWithValue = Field(newField.name, - ProtectedString(newField.protectedValue.isProtected, oldValue)) - val oldPosition = indexCustomFieldIdByName(oldField.name) - if (oldPosition >= 0) - mCustomFieldIds.removeAt(oldPosition) + val newCustomFieldWithValue = if (keepOldValue) + Field(newField.name, + ProtectedString(newField.protectedValue.isProtected, oldValue) + ) + else + newField + val oldPosition = getIndexViewFieldByName(oldField.name) + if (oldPosition >= 0) + mViewFields.removeAt(oldPosition) - val newCustomView = buildViewForCustomField(newCustomFieldWithValue) - newCustomView?.let { - parentGroup.addView(newCustomView, indexInParent) - mCustomFieldIds.add( - oldPosition, - FieldId( - newCustomView.id, - newCustomFieldWithValue - ) + val newCustomView = buildViewForCustomField(newCustomFieldWithValue) + newCustomView?.let { + parentGroup.addView(newCustomView, indexInParent) + mViewFields.add( + oldPosition, + ViewField( + newCustomView, + newCustomFieldWithValue ) - if (focus) - newCustomView.requestFocus() - } - return true + ) + if (focus) + newCustomView.requestFocus() } + return true } } return false } + /** + * Update a custom field and keep the old value + */ fun replaceCustomField(oldField: Field, newField: Field): Boolean { - val replace = replaceCustomField(oldField, newField, true) + val replace = replaceCustomField(oldField, newField, keepOldValue = true, focus = true) retrieveCustomFieldsFromView() return replace } fun removeCustomField(oldCustomField: Field) { - val indexOldField = indexCustomFieldIdByName(oldCustomField.name) + val indexOldField = getIndexViewFieldByName(oldCustomField.name) if (indexOldField >= 0) { - mCustomFieldIds[indexOldField].viewId.let { viewId -> - customFieldsContainerView.removeViewById(viewId) + mViewFields[indexOldField].let { fieldView -> + customFieldsContainerView.removeViewById(fieldView.view.id) } - mCustomFieldIds.removeAt(indexOldField) + mViewFields.removeAt(indexOldField) } retrieveCustomFieldsFromView() } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt index 4e20c8138..f1e967227 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -192,7 +192,7 @@ class TemplateEditView @JvmOverloads constructor(context: Context, } } - override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { + override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { refreshIcon() return super.populateViewsWithEntryInfo(showEmptyFields) } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt index 81879a8d4..51925f679 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt @@ -56,18 +56,20 @@ class TemplateView @JvmOverloads constructor(context: Context, setMaxLines(templateAttribute.options.getNumberLines()) // TODO Linkify value = field.protectedValue.stringValue + // Here the value is often empty if (field.protectedValue.isProtected) { if (mFirstTimeAskAllowCopyProtectedFields) { setCopyButtonState(TextFieldView.ButtonState.DEACTIVATE) - setCopyButtonClickListener { + setCopyButtonClickListener { _, _ -> mOnAskCopySafeClickListener?.invoke() } } else { if (mAllowCopyProtectedFields) { setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) - setCopyButtonClickListener { - mOnCopyActionClickListener?.invoke(field) + setCopyButtonClickListener { label, value -> + mOnCopyActionClickListener + ?.invoke(Field(label, ProtectedString(false, value))) } } else { setCopyButtonState(TextFieldView.ButtonState.GONE) @@ -76,8 +78,9 @@ class TemplateView @JvmOverloads constructor(context: Context, } } else { setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) - setCopyButtonClickListener { - mOnCopyActionClickListener?.invoke(field) + setCopyButtonClickListener { label, value -> + mOnCopyActionClickListener + ?.invoke(Field(label, ProtectedString(false, value))) } } } @@ -114,22 +117,22 @@ class TemplateView @JvmOverloads constructor(context: Context, return findViewWithTag(FIELD_PASSWORD_TAG)?.getCopyButtonView() } - override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { + override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List { val emptyCustomFields = super.populateViewsWithEntryInfo(false) // Hide empty custom fields emptyCustomFields.forEach { customFieldId -> - templateContainerView.findViewById(customFieldId.viewId) - ?.isVisible = false + customFieldId.view.isVisible = false } + removeOtpRunnable() mEntryInfo?.let { entryInfo -> // Assign specific OTP dynamic view - removeOtpRunnable() entryInfo.otpModel?.let { assignOtp(it) } } + return emptyCustomFields } @@ -146,11 +149,10 @@ class TemplateView @JvmOverloads constructor(context: Context, private var mOnOtpElementUpdated: ((OtpElement?) -> Unit)? = null private fun getOtpTokenView(): TextFieldView? { - val indexFieldViewId = indexCustomFieldIdByName(OTP_TOKEN_FIELD) - if (indexFieldViewId >= 0) { - // Template contains the custom view - val customFieldId = mCustomFieldIds[indexFieldViewId] - return findViewById(customFieldId.viewId) + getViewFieldByName(OTP_TOKEN_FIELD)?.let { viewField -> + val view = viewField.view + if (view is TextFieldView) + return view } return null } @@ -166,7 +168,7 @@ class TemplateView @JvmOverloads constructor(context: Context, label = otpElement.type.name value = otpElement.token setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) - setCopyButtonClickListener { + setCopyButtonClickListener { _, _ -> mOnCopyActionClickListener?.invoke(Field( otpElement.type.name, ProtectedString(false, otpElement.token))) diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt index f79735ed1..6b66def6f 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt @@ -1,6 +1,7 @@ package com.kunzisoft.keepass.view import android.content.Context +import android.os.Build import android.text.InputFilter import android.text.InputType import android.util.AttributeSet @@ -41,11 +42,11 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO } maxLines = 1 @@ -61,7 +62,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, resources.displayMetrics ).toInt() it.addRule(ALIGN_PARENT_RIGHT) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { it.addRule(ALIGN_PARENT_END) } } @@ -82,7 +83,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, id = labelViewId layoutParams = (layoutParams as LayoutParams?).also { it?.addRule(LEFT_OF, actionImageButtonId) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { it?.addRule(START_OF, actionImageButtonId) } } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt index fd4b4117c..e34207ac7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Jeremy Jamet / Kunzisoft. + * Copyright 2021 Jeremy Jamet / Kunzisoft. * * This file is part of KeePassDX. * @@ -20,16 +20,20 @@ package com.kunzisoft.keepass.view import android.content.Context +import android.os.Build import android.text.InputFilter import android.text.util.Linkify import android.util.AttributeSet -import android.view.LayoutInflater +import android.util.TypedValue +import android.view.ContextThemeWrapper import android.view.View -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView +import android.widget.RelativeLayout import androidx.annotation.StringRes +import androidx.appcompat.widget.AppCompatImageButton +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.content.ContextCompat import androidx.core.text.util.LinkifyCompat +import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.kunzisoft.keepass.R import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME @@ -39,22 +43,130 @@ import com.kunzisoft.keepass.utils.UriUtil class TextFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) - : LinearLayout(context, attrs, defStyle), GenericTextFieldView { + : RelativeLayout(context, attrs, defStyle), GenericTextFieldView { - private val labelView: TextView - private val valueView: TextView - private val showButtonView: ImageView - private val copyButtonView: ImageView + private var labelViewId = ViewCompat.generateViewId() + private var valueViewId = ViewCompat.generateViewId() + private var showButtonId = ViewCompat.generateViewId() + private var copyButtonId = ViewCompat.generateViewId() + + private val labelView = AppCompatTextView(context).apply { + setTextAppearance(context, + R.style.KeepassDXStyle_TextAppearance_LabelTextStyle) + layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ).also { + it.leftMargin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 4f, + resources.displayMetrics + ).toInt() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it.marginStart = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 4f, + resources.displayMetrics + ).toInt() + } + } + } + private val valueView = AppCompatTextView(context).apply { + setTextAppearance(context, + R.style.KeepassDXStyle_TextAppearance_TextEntryItem) + layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT).also { + it.topMargin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 4f, + resources.displayMetrics + ).toInt() + it.leftMargin = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + resources.displayMetrics + ).toInt() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it.marginStart = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + resources.displayMetrics + ).toInt() + } + } + setTextIsSelectable(true) + } + private var showButton = AppCompatImageButton( + ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { + layoutParams = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT) + setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_visibility_state)) + contentDescription = context.getString(R.string.menu_showpass) + } + private var copyButton = AppCompatImageButton( + ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { + layoutParams = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT) + setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_content_copy_white_24dp)) + contentDescription = context.getString(R.string.menu_copy) + } init { - val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? - inflater?.inflate(R.layout.view_entry_field, this) + buildViews() + addView(copyButton) + addView(showButton) + addView(labelView) + addView(valueView) + } - labelView = findViewById(R.id.entry_field_label) - valueView = findViewById(R.id.entry_field_value) - showButtonView = findViewById(R.id.entry_field_show) - copyButtonView = findViewById(R.id.entry_field_copy) - copyButtonView.visibility = View.GONE + private fun buildViews() { + copyButton.apply { + id = copyButtonId + layoutParams = (layoutParams as LayoutParams?).also { + it?.addRule(ALIGN_PARENT_RIGHT) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it?.addRule(ALIGN_PARENT_END) + } + } + } + showButton.apply { + id = showButtonId + layoutParams = (layoutParams as LayoutParams?).also { + if (copyButton.isVisible) { + it?.addRule(LEFT_OF, copyButtonId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it?.addRule(START_OF, copyButtonId) + } + } else { + it?.addRule(ALIGN_PARENT_RIGHT) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it?.addRule(ALIGN_PARENT_END) + } + } + } + } + labelView.apply { + id = labelViewId + layoutParams = (layoutParams as LayoutParams?).also { + it?.addRule(LEFT_OF, showButtonId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it?.addRule(START_OF, showButtonId) + } + } + } + valueView.apply { + id = valueViewId + layoutParams = (layoutParams as LayoutParams?).also { + it?.addRule(LEFT_OF, showButtonId) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + it?.addRule(START_OF, showButtonId) + } + it?.addRule(BELOW, labelViewId) + } + } } override fun applyFontVisibility(fontInVisibility: Boolean) { @@ -115,19 +227,20 @@ class TextFieldView @JvmOverloads constructor(context: Context, } fun setProtection(protection: Boolean, hiddenProtectedValue: Boolean = false) { - showButtonView.isVisible = protection - showButtonView.isSelected = hiddenProtectedValue - showButtonView.setOnClickListener { - showButtonView.isSelected = !showButtonView.isSelected + showButton.isVisible = protection + showButton.isSelected = hiddenProtectedValue + showButton.setOnClickListener { + showButton.isSelected = !showButton.isSelected changeProtectedValueParameters() } changeProtectedValueParameters() + invalidate() } private fun changeProtectedValueParameters() { valueView.apply { - if (showButtonView.isVisible) { - applyHiddenStyle(showButtonView.isSelected) + if (showButton.isVisible) { + applyHiddenStyle(showButton.isSelected) } else { linkify() } @@ -138,11 +251,11 @@ class TextFieldView @JvmOverloads constructor(context: Context, when { labelView.text.contains(APPLICATION_ID_FIELD_NAME) -> { val packageName = valueView.text.toString() - if (UriUtil.isExternalAppInstalled(context, packageName)) { + // TODO #996 if (UriUtil.isExternalAppInstalled(context, packageName)) { valueView.customLink { UriUtil.openExternalApp(context, packageName) } - } + //} } else -> { LinkifyCompat.addLinks(valueView, Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES) @@ -151,8 +264,8 @@ class TextFieldView @JvmOverloads constructor(context: Context, } fun getCopyButtonView(): View? { - if (copyButtonView.isVisible) { - return copyButtonView + if (copyButton.isVisible) { + return copyButton } return null } @@ -160,7 +273,7 @@ class TextFieldView @JvmOverloads constructor(context: Context, fun setCopyButtonState(buttonState: ButtonState) { when (buttonState) { ButtonState.ACTIVATE -> { - copyButtonView.apply { + copyButton.apply { visibility = VISIBLE isActivated = false } @@ -170,7 +283,7 @@ class TextFieldView @JvmOverloads constructor(context: Context, } } ButtonState.DEACTIVATE -> { - copyButtonView.apply { + copyButton.apply { visibility = VISIBLE // Reverse because isActivated show custom color and allow click isActivated = true @@ -181,7 +294,7 @@ class TextFieldView @JvmOverloads constructor(context: Context, } } ButtonState.GONE -> { - copyButtonView.apply { + copyButton.apply { visibility = GONE setOnClickListener(null) } @@ -191,18 +304,24 @@ class TextFieldView @JvmOverloads constructor(context: Context, } } } + invalidate() } - fun setCopyButtonClickListener(onActionClickListener: OnClickListener?) { - setOnActionClickListener(onActionClickListener, null) + fun setCopyButtonClickListener(onActionClickListener: ((label: String, value: String) -> Unit)?) { + val clickListener = if (onActionClickListener != null) + OnClickListener { onActionClickListener.invoke(label, value) } + else + null + setOnActionClickListener(clickListener, null) } override fun setOnActionClickListener( onActionClickListener: OnClickListener?, actionImageId: Int? ) { - copyButtonView.setOnClickListener(onActionClickListener) - copyButtonView.isVisible = onActionClickListener != null + copyButton.setOnClickListener(onActionClickListener) + copyButton.isVisible = onActionClickListener != null + invalidate() } override var isFieldVisible: Boolean @@ -213,6 +332,11 @@ class TextFieldView @JvmOverloads constructor(context: Context, isVisible = value } + override fun invalidate() { + super.invalidate() + buildViews() + } + enum class ButtonState { ACTIVATE, DEACTIVATE, GONE } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextSelectFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextSelectFieldView.kt index af8f2f059..e420dcc9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextSelectFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextSelectFieldView.kt @@ -2,21 +2,27 @@ package com.kunzisoft.keepass.view import android.content.Context import android.os.Build +import android.text.InputType import android.util.AttributeSet import android.util.TypedValue import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.EditorInfo import android.widget.* import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageButton -import androidx.appcompat.widget.AppCompatTextView import androidx.core.content.ContextCompat +import androidx.core.graphics.BlendModeColorFilterCompat +import androidx.core.graphics.BlendModeCompat import androidx.core.view.ViewCompat import androidx.core.view.isVisible +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import com.kunzisoft.keepass.R + class TextSelectFieldView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) @@ -28,30 +34,54 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context, private var actionImageButtonId = ViewCompat.generateViewId() private var mDefaultPosition = 0 - private val labelView = AppCompatTextView(context).apply { - setTextAppearance(context, R.style.KeepassDXStyle_TextAppearance_LabelTextStyle) + private val labelView = TextInputLayout(context).apply { layoutParams = LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT).also { - val leftMargin = 4f - it.leftMargin = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - leftMargin, - resources.displayMetrics - ).toInt() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - it.marginStart = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - leftMargin, - resources.displayMetrics - ).toInt() - } + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT) + } + private val valueView = TextInputEditText( + ContextThemeWrapper(context, + R.style.KeepassDXStyle_TextInputLayout) + ).apply { + layoutParams = LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT) + inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + imeOptions = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + importantForAutofill = IMPORTANT_FOR_AUTOFILL_NO } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO + } + val drawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_white_24dp) + ?.apply { + mutate().colorFilter = BlendModeColorFilterCompat + .createBlendModeColorFilterCompat(currentTextColor, BlendModeCompat.SRC_IN) + } + setCompoundDrawablesWithIntrinsicBounds( + null, + null, + drawable, + null + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + setCompoundDrawablesRelativeWithIntrinsicBounds( + null, + null, + drawable, + null + ) + } + isFocusable = false + inputType = InputType.TYPE_NULL + maxLines = 1 } private val valueSpinnerView = Spinner(context).apply { layoutParams = LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT) + 0, + 0 + ) } private var actionImageButton = AppCompatImageButton( ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { @@ -64,7 +94,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context, resources.displayMetrics ).toInt() it.addRule(ALIGN_PARENT_RIGHT) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { it.addRule(ALIGN_PARENT_END) } } @@ -75,15 +105,22 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context, init { // Manually write view to avoid view id bugs buildViews() + labelView.addView(valueView) addView(labelView) addView(valueSpinnerView) addView(actionImageButton) + valueView.apply { + setOnClickListener { + valueSpinnerView.performClick() + } + } valueSpinnerView.apply { adapter = valueSpinnerAdapter onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - valueSpinnerAdapter.getItem(position) + val stringValue = valueSpinnerAdapter.getItem(position) + valueView.setText(stringValue) } override fun onNothingSelected(parent: AdapterView<*>?) {} } @@ -95,7 +132,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context, id = labelViewId layoutParams = (layoutParams as LayoutParams?).also { it?.addRule(LEFT_OF, actionImageButtonId) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { it?.addRule(START_OF, actionImageButtonId) } } @@ -104,7 +141,7 @@ class TextSelectFieldView @JvmOverloads constructor(context: Context, id = valueViewId layoutParams = (layoutParams as LayoutParams?).also { it?.addRule(LEFT_OF, actionImageButtonId) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { it?.addRule(START_OF, actionImageButtonId) } it?.addRule(BELOW, labelViewId) diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt index 663057182..734814cbd 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -5,29 +5,23 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.kunzisoft.keepass.app.database.IOActionTask 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.NodeId import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.otp.OtpElement -import java.util.* class EntryEditViewModel: NodeEditViewModel() { private val mTempAttachments = mutableListOf() - val entryInfo : LiveData get() = _entryInfo - private val _entryInfo = MutableLiveData() + val templatesEntry : LiveData get() = _templatesEntry + private val _templatesEntry = MutableLiveData() val requestEntryInfoUpdate : LiveData get() = _requestEntryInfoUpdate private val _requestEntryInfoUpdate = SingleLiveEvent() val onEntrySaved : LiveData get() = _onEntrySaved private val _onEntrySaved = SingleLiveEvent() - val templates : LiveData get() = _templates - private val _templates = MutableLiveData() val onTemplateChanged : LiveData