From 963b5e84af6bc1fb22e3ec0e850321dc772ce578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Fri, 17 Sep 2021 18:50:49 +0200 Subject: [PATCH 1/6] add "download icon" action next to the URL field --- .../keepass/activities/fragments/EntryEditFragment.kt | 3 +++ .../com/kunzisoft/keepass/view/TemplateEditView.kt | 10 ++++++++++ .../kunzisoft/keepass/viewmodels/EntryEditViewModel.kt | 4 ++++ 3 files changed, 17 insertions(+) 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 1d8bea4bf..6e39f8ccf 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 @@ -105,6 +105,9 @@ class EntryEditFragment: DatabaseFragment() { setOnPasswordGenerationActionClickListener { field -> mEntryEditViewModel.requestPasswordSelection(field) } + setOnDownloadIconActionClickListener { url -> + mEntryEditViewModel.requestDownloadIcon(url) + } setOnDateInstantClickListener { dateInstant -> mEntryEditViewModel.requestDateTimeSelection(dateInstant) } 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 956621a09..07655097c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -36,6 +36,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context, this.mOnPasswordGenerationActionClickListener = listener } + private var mOnDownloadIconActionClickListener: ((String) -> Unit)? = null + fun setOnDownloadIconActionClickListener(listener: ((String) -> Unit)?) { + this.mOnDownloadIconActionClickListener = listener + } + private var mOnDateInstantClickListener: ((DateInstant) -> Unit)? = null fun setOnDateInstantClickListener(listener: ((DateInstant) -> Unit)?) { this.mOnDateInstantClickListener = listener @@ -113,6 +118,11 @@ class TemplateEditView @JvmOverloads constructor(context: Context, mOnPasswordGenerationActionClickListener?.invoke(field) }, R.drawable.ic_generate_password_white_24dp) } + if (templateAttribute.options.isLink()) { + setOnActionClickListener({ + mOnDownloadIconActionClickListener?.invoke(value) + }, R.drawable.ic_downloading_white_24dp) + } } } 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 241dd2dfd..407084fcc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -257,6 +257,10 @@ class EntryEditViewModel: NodeEditViewModel() { _onPasswordSelected.value = passwordField } + fun requestDownloadIcon(url: String) { + // TODO + } + fun requestCustomFieldEdition(customField: Field) { _requestCustomFieldEdition.value = customField } From 73f867934a8ba87acace0da7e66fa9cb6781b236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Fri, 22 Oct 2021 20:28:09 +0200 Subject: [PATCH 2/6] Add download and save logic --- app/src/main/AndroidManifest.xml | 1 + .../activities/fragments/EntryEditFragment.kt | 14 ++- .../keepass/app/database/IOActionTask.kt | 2 +- .../keepass/database/element/Database.kt | 12 +++ .../database/element/database/DatabaseKDBX.kt | 4 + .../database/element/icon/IconsManager.kt | 10 ++ .../keepass/viewmodels/EntryEditViewModel.kt | 100 ++++++++++++++++++ app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 139 insertions(+), 6 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72898c755..c25daafc7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + - mEntryEditViewModel.requestDownloadIcon(url) + mEntryEditViewModel.requestDownloadIcon(url, requireContext(), mDatabase) } setOnDateInstantClickListener { dateInstant -> mEntryEditViewModel.requestDateTimeSelection(dateInstant) @@ -154,6 +152,12 @@ class EntryEditFragment: DatabaseFragment() { templateView.setPasswordField(passwordField) } + mEntryEditViewModel.onIconDownloaded.observe(viewLifecycleOwner) { state -> + if (state.errorStringId != null) { + Snackbar.make(rootView, state.errorStringId, Snackbar.LENGTH_LONG).asError().show() + } + } + mEntryEditViewModel.onDateSelected.observe(viewLifecycleOwner) { viewModelDate -> // Save the date templateView.setCurrentDateTimeValue(viewModelDate) diff --git a/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt b/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt index 4cc0914d4..7f91e3e9e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt +++ b/app/src/main/java/com/kunzisoft/keepass/app/database/IOActionTask.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.* * Class to invoke action in a separate IO thread */ class IOActionTask( - private val action: () -> T , + private val action: suspend () -> T , private val afterActionDatabaseListener: ((T?) -> Unit)? = null) { private val mainScope = CoroutineScope(Dispatchers.Main) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt index 1d960f64d..2c19b2e9d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/Database.kt @@ -63,6 +63,8 @@ import com.kunzisoft.keepass.utils.readBytes4ToUInt import java.io.* import java.util.* import kotlin.collections.ArrayList +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class Database { @@ -138,10 +140,20 @@ class Database { mDatabaseKDBX?.buildNewCustomIcon(null, result) } + suspend fun buildNewCustomIcon(): Pair { + return suspendCoroutine { coroutine -> + buildNewCustomIcon { icon, binary -> coroutine.resume(Pair(icon, binary)) } + } + } + fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { return mDatabaseKDBX?.isCustomIconBinaryDuplicate(binaryData) ?: false } + fun getIcon(binary: BinaryData): IconImageCustom? { + return mDatabaseKDBX?.getCustomIcon(binary) + } + fun removeCustomIcon(customIcon: IconImageCustom) { iconDrawableFactory.clearFromCache(customIcon) iconsManager.removeCustomIcon(binaryCache, customIcon.uuid) diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt index e254f20a8..7c199cea9 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/database/DatabaseKDBX.kt @@ -342,6 +342,10 @@ class DatabaseKDBX : DatabaseVersioned { return this.iconsManager.getIcon(iconUuid) } + fun getCustomIcon(binary: BinaryData): IconImageCustom? { + return this.iconsManager.getIcon(binary) + } + fun isTemplatesGroupEnabled(): Boolean { return entryTemplatesGroup != UUID_ZERO } diff --git a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt index 58aafa702..3c7dc79ea 100644 --- a/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt +++ b/app/src/main/java/com/kunzisoft/keepass/database/element/icon/IconsManager.kt @@ -68,6 +68,16 @@ class IconsManager(binaryCache: BinaryCache) { return IconImageCustom(iconUuid) } + fun getIcon(data: BinaryData): IconImageCustom? { + var toReturn: IconImageCustom? = null + doForEachCustomIcon { customIcon, binary -> + if (data.binaryHash() == binary.binaryHash()) { + toReturn = customIcon + } + } + return toReturn + } + fun isCustomIconBinaryDuplicate(binaryData: BinaryData): Boolean { return customCache.isBinaryDuplicate(binaryData) } 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 407084fcc..e0809f455 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -1,16 +1,27 @@ package com.kunzisoft.keepass.viewmodels +import android.content.Context import android.net.Uri +import android.util.Log +import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import com.kunzisoft.keepass.R 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.IconImageCustom 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 com.kunzisoft.keepass.tasks.BinaryDatabaseManager +import com.kunzisoft.keepass.utils.UriUtil +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.URL import java.util.* @@ -53,6 +64,8 @@ class EntryEditViewModel: NodeEditViewModel() { val onOtpCreated : LiveData get() = _onOtpCreated private val _onOtpCreated = SingleLiveEvent() + val onIconDownloaded: LiveData get() = _onIconDownload + private val _onIconDownload = SingleLiveEvent() val onBuildNewAttachment : LiveData get() = _onBuildNewAttachment private val _onBuildNewAttachment = SingleLiveEvent() val onStartUploadAttachment : LiveData get() = _onStartUploadAttachment @@ -228,6 +241,86 @@ class EntryEditViewModel: NodeEditViewModel() { ).execute() } + fun requestDownloadIcon(url: String, context: Context, database: Database?) { + if (database == null) return + IOActionTask( + action = { + val file = downloadFavicon(url, context) + if (file == null) { + DownloadIconState(errorStringId = R.string.download_icon_error) + } else { + saveFavicon(file, context, database) + } + }, + afterActionDatabaseListener = { state -> + Log.d(TAG, "Download favicon state: $state") + state?.downloadedIcon?.getIconImageToDraw()?.let { selectIcon(it) } + _onIconDownload.value = state + } + ).execute() + } + + private fun downloadFavicon(url: String, context: Context): File? { + return try { + Log.d(TAG, "Downloading icon: $url") + val authority = extractAuthorityFromUrl(url) + val faviconUrl = URL("https://icons.duckduckgo.com/ip3/${authority}.ico") + val file = File(context.cacheDir, "temp_favicon.ico") + + // Copy the icon to a file cache + faviconUrl.openStream().use { input -> + FileOutputStream(file).use { output -> + input.copyTo(output) + } + } + file + } catch (e: IOException) { + Log.d(TAG, "Download icon exception: $e") + null + } + } + + private suspend fun saveFavicon( + file: File, + context: Context, + database: Database + ): DownloadIconState { + val documentFile = UriUtil.getFileData(context, file.toUri()) + if (documentFile == null || documentFile.length() > MAX_ICON_SIZE) { + Log.d(TAG, "Error while saving favicon") + return DownloadIconState(errorStringId = R.string.download_icon_error) + } + + var (customIcon, binary) = database.buildNewCustomIcon() + BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile( + context.contentResolver, + database, + documentFile.uri, + binary + ) + + if (binary != null && database.isCustomIconBinaryDuplicate(binary)) { + Log.d(TAG, "Favicon is duplicated. Reusing existing one") + // Remove duplicated icon from database + customIcon?.let { database.removeCustomIcon(it) } + + // Look for the original icon using its binary data + customIcon = database.getIcon(binary) + } + + return DownloadIconState(downloadedIcon = customIcon) + } + + private fun extractAuthorityFromUrl(url: String): String { + // Using https as fallback to create the URL + val urlWithProtocol = when { + url.startsWith("https") || url.startsWith("http") -> url + else -> "https://$url" + } + + return URL(urlWithProtocol).authority + } + private fun removeTempAttachmentsNotCompleted(entryInfo: EntryInfo) { // Do not save entry in upload progression mTempAttachments.forEach { attachmentState -> @@ -327,7 +420,14 @@ class EntryEditViewModel: NodeEditViewModel() { data class AttachmentUpload(val attachmentToUploadUri: Uri, val attachment: Attachment) data class AttachmentPosition(val entryAttachmentState: EntryAttachmentState, val viewPosition: Float) + data class DownloadIconState( + val downloadedIcon: IconImageCustom? = null, + val errorStringId: Int? = null + ) + companion object { private val TAG = EntryEditViewModel::class.java.name + + private const val MAX_ICON_SIZE = 5242880 } } \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8b3dccc27..87963f2a9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -542,6 +542,7 @@ Recargar la base de datos El tipo de OTP existente no es reconocido por este formulario, su validación ya no puede generar correctamente el token. ¡Cancelado! + No se pudo descargar el icono Los datos del archivo ya existen. Se produjo un error al cargar los datos del archivo. Propiedades de KeePassDX para gestionar la configuración de la aplicación diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 562959967..74d5db18b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -570,6 +570,7 @@ Finalizing… Complete! Canceled! + Could not download icon B KiB MiB From b6f91907a2fe5930fb4d4edcd31c57a7f69803af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Fri, 22 Oct 2021 20:53:16 +0200 Subject: [PATCH 3/6] downloadFavicon as suspend --- .../keepass/viewmodels/EntryEditViewModel.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 e0809f455..ba574019e 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -18,6 +18,8 @@ import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.tasks.BinaryDatabaseManager import com.kunzisoft.keepass.utils.UriUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -260,8 +262,14 @@ class EntryEditViewModel: NodeEditViewModel() { ).execute() } - private fun downloadFavicon(url: String, context: Context): File? { - return try { + /** + * Needs to specify an IO Dispatcher to define this function as suspend + */ + private suspend fun downloadFavicon( + url: String, + context: Context + ): File? = withContext(Dispatchers.IO) { + try { Log.d(TAG, "Downloading icon: $url") val authority = extractAuthorityFromUrl(url) val faviconUrl = URL("https://icons.duckduckgo.com/ip3/${authority}.ico") From 5c301d87e433ceb8227f03a1ed0fa70ec2b9e4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Sun, 24 Oct 2021 15:59:00 +0200 Subject: [PATCH 4/6] Remove unused function --- .../com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt | 4 ---- 1 file changed, 4 deletions(-) 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 ba574019e..c79f7f08d 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -358,10 +358,6 @@ class EntryEditViewModel: NodeEditViewModel() { _onPasswordSelected.value = passwordField } - fun requestDownloadIcon(url: String) { - // TODO - } - fun requestCustomFieldEdition(customField: Field) { _requestCustomFieldEdition.value = customField } From f7e86e8f441847c14bf97e652b1b7fc825c96dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Sun, 24 Oct 2021 17:43:44 +0200 Subject: [PATCH 5/6] Improve download state management --- .../activities/fragments/EntryEditFragment.kt | 23 ++++++++++++-- .../com/kunzisoft/keepass/view/ViewUtil.kt | 9 ++++++ .../keepass/viewmodels/EntryEditViewModel.kt | 30 ++++++++++++------- 3 files changed, 50 insertions(+), 12 deletions(-) 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 bc8c2b9b1..cbab1ed89 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 @@ -153,8 +153,27 @@ class EntryEditFragment: DatabaseFragment() { } mEntryEditViewModel.onIconDownloaded.observe(viewLifecycleOwner) { state -> - if (state.errorStringId != null) { - Snackbar.make(rootView, state.errorStringId, Snackbar.LENGTH_LONG).asError().show() + when (state.downloadState) { + EntryEditViewModel.DownloadState.NONE -> { + // Do nothing + } + EntryEditViewModel.DownloadState.START -> { + // TODO show loading + } + EntryEditViewModel.DownloadState.COMPLETE -> { + // TODO hide loading + Snackbar + .make(rootView, R.string.download_complete, Snackbar.LENGTH_LONG) + .asSuccess() + .show() + } + EntryEditViewModel.DownloadState.ERROR -> { + // TODO hide loading + Snackbar + .make(rootView, R.string.download_icon_error, Snackbar.LENGTH_LONG) + .asError() + .show() + } } } diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index b433aacfc..f1aae0ce8 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -38,6 +38,7 @@ import android.view.animation.AccelerateDecelerateInterpolator import android.widget.TextView import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding import com.google.android.material.snackbar.Snackbar @@ -92,6 +93,14 @@ fun TextView.customLink(listener: (View) -> Unit) { this.setText(spannableString, TextView.BufferType.SPANNABLE) } +fun Snackbar.asSuccess(): Snackbar { + this.view.apply { + setBackgroundColor(ContextCompat.getColor(context, R.color.blue_light)) + findViewById(R.id.snackbar_text).setTextColor(Color.WHITE) + } + return this +} + fun Snackbar.asError(): Snackbar { this.view.apply { setBackgroundColor(Color.RED) 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 c79f7f08d..4b9df5961 100644 --- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt +++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt @@ -244,14 +244,23 @@ class EntryEditViewModel: NodeEditViewModel() { } fun requestDownloadIcon(url: String, context: Context, database: Database?) { - if (database == null) return + if (database == null || _onIconDownload.value?.downloadState == DownloadState.START) return + _onIconDownload.value = DownloadIconState(downloadState = DownloadState.START) IOActionTask( action = { val file = downloadFavicon(url, context) if (file == null) { - DownloadIconState(errorStringId = R.string.download_icon_error) + DownloadIconState(downloadState = DownloadState.ERROR) } else { - saveFavicon(file, context, database) + val downloadedIcon = saveFavicon(file, context, database) + if (downloadedIcon == null) { + DownloadIconState(downloadState = DownloadState.ERROR) + } else { + DownloadIconState( + downloadedIcon = downloadedIcon, + downloadState = DownloadState.COMPLETE + ) + } } }, afterActionDatabaseListener = { state -> @@ -292,12 +301,9 @@ class EntryEditViewModel: NodeEditViewModel() { file: File, context: Context, database: Database - ): DownloadIconState { + ): IconImageCustom? { val documentFile = UriUtil.getFileData(context, file.toUri()) - if (documentFile == null || documentFile.length() > MAX_ICON_SIZE) { - Log.d(TAG, "Error while saving favicon") - return DownloadIconState(errorStringId = R.string.download_icon_error) - } + if (documentFile == null || documentFile.length() > MAX_ICON_SIZE) return null var (customIcon, binary) = database.buildNewCustomIcon() BinaryDatabaseManager.resizeBitmapAndStoreDataInBinaryFile( @@ -316,7 +322,7 @@ class EntryEditViewModel: NodeEditViewModel() { customIcon = database.getIcon(binary) } - return DownloadIconState(downloadedIcon = customIcon) + return customIcon } private fun extractAuthorityFromUrl(url: String): String { @@ -426,9 +432,13 @@ class EntryEditViewModel: NodeEditViewModel() { data class DownloadIconState( val downloadedIcon: IconImageCustom? = null, - val errorStringId: Int? = null + val downloadState: DownloadState = DownloadState.NONE ) + enum class DownloadState { + NONE, START, COMPLETE, ERROR + } + companion object { private val TAG = EntryEditViewModel::class.java.name From 6446f4db798bfc219e32ec90e5f996635e801830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Belmonte?= Date: Thu, 28 Oct 2021 20:04:12 +0200 Subject: [PATCH 6/6] Show progress while downloading icon --- .../activities/fragments/EntryEditFragment.kt | 6 +- .../keepass/view/TemplateEditView.kt | 7 ++ .../keepass/view/TextEditFieldView.kt | 70 ++++++++++++++----- app/src/main/res/values/styles.xml | 4 ++ 4 files changed, 67 insertions(+), 20 deletions(-) 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 cbab1ed89..fd5d1cd1e 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 @@ -158,17 +158,17 @@ class EntryEditFragment: DatabaseFragment() { // Do nothing } EntryEditViewModel.DownloadState.START -> { - // TODO show loading + templateView.setDownloadIconProgressVisible(true) } EntryEditViewModel.DownloadState.COMPLETE -> { - // TODO hide loading + templateView.setDownloadIconProgressVisible(false) Snackbar .make(rootView, R.string.download_complete, Snackbar.LENGTH_LONG) .asSuccess() .show() } EntryEditViewModel.DownloadState.ERROR -> { - // TODO hide loading + templateView.setDownloadIconProgressVisible(false) Snackbar .make(rootView, R.string.download_icon_error, Snackbar.LENGTH_LONG) .asError() 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 07655097c..42c305b65 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt @@ -164,6 +164,13 @@ class TemplateEditView @JvmOverloads constructor(context: Context, return Field(TemplateField.LABEL_PASSWORD, ProtectedString(true, passwordView?.value ?: "")) } + fun setDownloadIconProgressVisible(visible: Boolean) { + val urlView: TextEditFieldView? = findViewWithTag(FIELD_URL_TAG) + if (urlView != null) { + urlView.isProgressVisible = visible + } + } + private fun setCurrentDateTimeSelection(action: (dateInstant: DateInstant) -> DateInstant) { mTempDateTimeViewId?.let { viewId -> val dateTimeView = getFieldViewById(viewId) 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 afa8e31be..8bd38d782 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TextEditFieldView.kt @@ -7,14 +7,18 @@ import android.text.InputType import android.util.AttributeSet import android.util.TypedValue import android.view.ContextThemeWrapper +import android.view.Gravity import android.view.View import android.view.inputmethod.EditorInfo +import android.widget.FrameLayout import android.widget.LinearLayout +import android.widget.ProgressBar import android.widget.RelativeLayout import androidx.annotation.DrawableRes import androidx.appcompat.widget.AppCompatImageButton import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat +import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout @@ -27,7 +31,9 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, private var labelViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId() + private var actionImageContainerId = ViewCompat.generateViewId() private var actionImageButtonId = ViewCompat.generateViewId() + private var actionImageProgressId = ViewCompat.generateViewId() private val labelView = TextInputLayout(context).apply { layoutParams = LayoutParams( @@ -51,15 +57,15 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, } maxLines = 1 } - private var actionImageButton = AppCompatImageButton( - ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply { + private var actionImageContainer = FrameLayout(context).apply { layoutParams = LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT).also { + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ).also { it.topMargin = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - 12f, - resources.displayMetrics + TypedValue.COMPLEX_UNIT_DIP, + 12f, + resources.displayMetrics ).toInt() it.addRule(ALIGN_PARENT_RIGHT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { @@ -67,33 +73,51 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, } } visibility = View.GONE + } + private var actionImageButton = AppCompatImageButton( + ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0 + ).apply { + layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) contentDescription = context.getString(R.string.menu_edit) } + private var actionImageProgress = ProgressBar( + ContextThemeWrapper(context, R.style.KeepassDXStyle_ProgressBar_Circle_Indeterminate) + ).apply { + val size = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 24f, + resources.displayMetrics + ).toInt() + layoutParams = FrameLayout.LayoutParams(size, size).apply { + gravity = Gravity.CENTER + } + visibility = View.GONE + } init { // Manually write view to avoid view id bugs buildViews() labelView.addView(valueView) addView(labelView) - addView(actionImageButton) + actionImageContainer.addView(actionImageProgress) + actionImageContainer.addView(actionImageButton) + addView(actionImageContainer) } private fun buildViews() { labelView.apply { id = labelViewId layoutParams = (layoutParams as LayoutParams?).also { - it?.addRule(LEFT_OF, actionImageButtonId) + it?.addRule(LEFT_OF, actionImageContainerId) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - it?.addRule(START_OF, actionImageButtonId) + it?.addRule(START_OF, actionImageContainerId) } } } - valueView.apply { - id = valueViewId - } - actionImageButton.apply { - id = actionImageButtonId - } + valueView.id = valueViewId + actionImageContainer.id = actionImageContainerId + actionImageButton.id = actionImageButtonId + actionImageProgress.id = actionImageProgressId } override fun applyFontVisibility(fontInVisibility: Boolean) { @@ -114,7 +138,9 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, // Define views Ids with label value labelViewId = "labelViewId $value".hashCode() valueViewId = "valueViewId $value".hashCode() + actionImageContainerId = "actionImageContainerId $value".hashCode() actionImageButtonId = "actionImageButtonId $value".hashCode() + actionImageProgressId = "actionImageProgressId $value".hashCode() buildViews() } @@ -174,7 +200,7 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, actionImageButton.setImageDrawable(ContextCompat.getDrawable(context, it)) } actionImageButton.setOnClickListener(onActionClickListener) - actionImageButton.visibility = if (onActionClickListener == null) View.GONE else View.VISIBLE + actionImageContainer.isVisible = onActionClickListener != null } override var isFieldVisible: Boolean @@ -185,6 +211,16 @@ class TextEditFieldView @JvmOverloads constructor(context: Context, isVisible = value } + var isProgressVisible: Boolean + get() { + return actionImageProgress.isVisible + } + set(value) { + // Toggle visibility between the button and the progress + actionImageProgress.isVisible = value + actionImageButton.isInvisible = value + } + companion object { const val MAX_CHARS_LIMIT = Integer.MAX_VALUE const val MAX_LINES_LIMIT = Integer.MAX_VALUE diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f133a6d60..13cc2040b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -496,6 +496,10 @@ @drawable/foreground_progress_circle @drawable/background_progress_circle +