Remove custom icons

This commit is contained in:
J-Jamet
2021-03-07 16:56:51 +01:00
parent 6357a30acb
commit d2e7e925f7
13 changed files with 283 additions and 88 deletions

View File

@@ -20,8 +20,11 @@
package com.kunzisoft.keepass.activities
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
@@ -39,10 +42,13 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.BinaryStreamManager
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.updateLockPaddingLeft
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
import kotlinx.coroutines.*
import java.io.File
class IconPickerActivity : LockingActivity() {
@@ -54,7 +60,11 @@ class IconPickerActivity : LockingActivity() {
private var mIconImage: IconImage = IconImage()
private val mainScope = CoroutineScope(Dispatchers.Main)
private val iconPickerViewModel: IconPickerViewModel by viewModels()
private var mCustomIconsSelectionMode = false
private var mIconsSelected: List<IconImageCustom> = ArrayList()
private var mDatabase: Database? = null
@@ -68,7 +78,6 @@ class IconPickerActivity : LockingActivity() {
mDatabase = Database.getInstance()
toolbar = findViewById(R.id.toolbar)
toolbar.title = getString(R.string.about)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@@ -118,7 +127,7 @@ class IconPickerActivity : LockingActivity() {
mSelectFileHelper = SelectFileHelper(this)
iconPickerViewModel.iconStandardSelected.observe(this) { iconStandard ->
iconPickerViewModel.standardIconPicked.observe(this) { iconStandard ->
mIconImage.standard = iconStandard
// Remove the custom icon if a standard one is selected
mIconImage.custom = IconImageCustom()
@@ -127,7 +136,7 @@ class IconPickerActivity : LockingActivity() {
})
finish()
}
iconPickerViewModel.iconCustomSelected.observe(this) { iconCustom ->
iconPickerViewModel.customIconPicked.observe(this) { iconCustom ->
// Keep the standard icon if a custom one is selected
mIconImage.custom = iconCustom
setResult(Activity.RESULT_OK, Intent().apply {
@@ -135,12 +144,32 @@ class IconPickerActivity : LockingActivity() {
})
finish()
}
iconPickerViewModel.iconCustomAdded.observe(this) { iconCustomAdded ->
iconPickerViewModel.customIconsSelected.observe(this) { iconsSelected ->
mIconsSelected = iconsSelected
if (iconsSelected.isEmpty()) {
mCustomIconsSelectionMode = false
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.title = ""
} else {
mCustomIconsSelectionMode = true
supportActionBar?.setDisplayShowTitleEnabled(true)
toolbar.title = iconsSelected.size.toString()
}
invalidateOptionsMenu()
}
iconPickerViewModel.customIconAdded.observe(this) { iconCustomAdded ->
if (iconCustomAdded.error) {
Snackbar.make(coordinatorLayout, R.string.error_upload_file, Snackbar.LENGTH_LONG).asError().show()
}
uploadButton.isEnabled = true
}
iconPickerViewModel.customIconRemoved.observe(this) { iconCustomRemoved ->
if (iconCustomRemoved.error) {
Snackbar.make(coordinatorLayout, R.string.error_remove_file, Snackbar.LENGTH_LONG).asError().show()
}
uploadButton.isEnabled = true
iconPickerViewModel.deselectAllCustomIcons()
}
}
override fun onSaveInstanceState(outState: Bundle) {
@@ -163,16 +192,72 @@ class IconPickerActivity : LockingActivity() {
toolbar.updateLockPaddingLeft()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
if (mCustomIconsSelectionMode) {
menuInflater.inflate(R.menu.icon, menu)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
if (mCustomIconsSelectionMode) {
iconPickerViewModel.deselectAllCustomIcons()
} else {
onBackPressed()
}
}
R.id.menu_delete -> {
mIconsSelected.forEach { iconToRemove ->
removeCustomIcon(iconToRemove)
}
}
}
return super.onOptionsItemSelected(item)
}
private fun addCustomIcon(contentResolver: ContentResolver,
iconDir: File,
iconToUploadUri: Uri) {
uploadButton.isEnabled = false
mainScope.launch {
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<IconImageCustom?> = async {
mDatabase?.buildNewCustomIcon(iconDir)?.let { customIcon ->
BinaryStreamManager.resizeBitmapAndStoreDataInBinaryFile(contentResolver,
iconToUploadUri, customIcon.binaryFile)
customIcon
}
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { customIcon ->
var error = false
if (customIcon.binaryFile.length <= 0) {
mDatabase?.removeCustomIcon(customIcon)
error = true
}
iconPickerViewModel.addCustomIcon(
IconPickerViewModel.IconCustomState(customIcon, error)
)
}
}
}
}
}
private fun removeCustomIcon(iconImageCustom: IconImageCustom) {
uploadButton.isEnabled = false
mDatabase?.removeCustomIcon(iconImageCustom)
iconPickerViewModel.removeCustomIcon(
IconPickerViewModel.IconCustomState(iconImageCustom, false)
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@@ -182,13 +267,10 @@ class IconPickerActivity : LockingActivity() {
if (documentFile.length() > MAX_ICON_SIZE) {
Snackbar.make(coordinatorLayout, R.string.error_file_to_big, Snackbar.LENGTH_LONG).asError().show()
} else {
mDatabase?.let { database ->
iconPickerViewModel.addCustomIcon(database,
contentResolver,
UriUtil.getBinaryDir(this),
iconToUploadUri)
uploadButton.isEnabled = false
}
addCustomIcon(
contentResolver,
UriUtil.getBinaryDir(this),
iconToUploadUri)
}
}
}

View File

@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.IconAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
@@ -40,17 +39,36 @@ class IconCustomFragment : IconFragment<IconImageCustom>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerViewModel.iconCustomAdded.observe(viewLifecycleOwner) { iconCustom ->
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { iconCustom ->
if (!iconCustom.error) {
iconAdapter.addIcon(iconCustom.iconCustom)
iconsGridView.smoothScrollToPosition(iconAdapter.lastPosition)
iconPickerAdapter.addIcon(iconCustom.iconCustom)
iconsGridView.smoothScrollToPosition(iconPickerAdapter.lastPosition)
}
}
iconAdapter.iconPickerListener = object : IconAdapter.IconPickerListener<IconImageCustom> {
override fun iconPicked(icon: IconImageCustom) {
iconPickerViewModel.selectIconCustom(icon)
iconPickerViewModel.customIconsSelected.observe(viewLifecycleOwner) { customIconsSelected ->
if (customIconsSelected.isEmpty()) {
iconPickerAdapter.deselectAllIcons()
}
}
iconPickerViewModel.customIconRemoved.observe(viewLifecycleOwner) { customIconRemoved ->
iconPickerAdapter.removeIcon(customIconRemoved.iconCustom)
}
}
override fun onIconClickListener(icon: IconImageCustom) {
if (iconActionSelectionMode) {
// Same long click behavior after each single click
onIconLongClickListener(icon)
} else {
iconPickerViewModel.pickCustomIcon(icon)
}
}
override fun onIconLongClickListener(icon: IconImageCustom) {
// Select or deselect item if already selected
icon.selected = !icon.selected
iconPickerAdapter.updateIcon(icon)
iconActionSelectionMode = iconPickerAdapter.containsAnySelectedIcon()
iconPickerViewModel.selectCustomIcons(iconPickerAdapter.getSelectedIcons())
}
}

View File

@@ -29,15 +29,17 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.IconAdapter
import com.kunzisoft.keepass.adapters.IconPickerAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.viewmodels.IconPickerViewModel
abstract class IconFragment<T: IconImageDraw> : StylishFragment() {
abstract class IconFragment<T: IconImageDraw> : StylishFragment(),
IconPickerAdapter.IconPickerListener<T> {
protected lateinit var iconsGridView: RecyclerView
protected lateinit var iconAdapter: IconAdapter<T>
protected lateinit var iconPickerAdapter: IconPickerAdapter<T>
protected var iconActionSelectionMode = false
protected val database = Database.getInstance()
@@ -55,11 +57,11 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment() {
val tintColor = ta?.getColor(0, Color.BLACK) ?: Color.BLACK
ta?.recycle()
iconAdapter = IconAdapter<T>(context, tintColor).apply {
iconPickerAdapter = IconPickerAdapter<T>(context, tintColor).apply {
iconDrawableFactory = database.iconDrawableFactory
}
iconAdapter.setList(defineIconList(database))
iconPickerAdapter.setList(defineIconList(database))
}
override fun onCreateView(inflater: LayoutInflater,
@@ -67,7 +69,17 @@ abstract class IconFragment<T: IconImageDraw> : StylishFragment() {
savedInstanceState: Bundle?): View {
val root = inflater.inflate(retrieveMainLayoutId(), container, false)
iconsGridView = root.findViewById(R.id.icons_grid_view)
iconsGridView.adapter = iconAdapter
iconsGridView.adapter = iconPickerAdapter
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconPickerAdapter.iconPickerListener = this
}
fun onIconDeleteClicked() {
iconActionSelectionMode = false
}
}

View File

@@ -53,7 +53,7 @@ class IconPickerFragment : StylishFragment() {
remove(ICON_TAB_ARG)
}
iconPickerViewModel.iconCustomAdded.observe(viewLifecycleOwner) { _ ->
iconPickerViewModel.customIconAdded.observe(viewLifecycleOwner) { _ ->
viewPager.currentItem = 1
}
}

View File

@@ -19,10 +19,7 @@
*/
package com.kunzisoft.keepass.activities.fragments
import android.os.Bundle
import android.view.View
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.IconAdapter
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
@@ -37,13 +34,9 @@ class IconStandardFragment : IconFragment<IconImageStandard>() {
return database.getStandardIconList()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
iconAdapter.iconPickerListener = object : IconAdapter.IconPickerListener<IconImageStandard> {
override fun iconPicked(icon: IconImageStandard) {
iconPickerViewModel.selectIconStandard(icon)
}
}
override fun onIconLongClickListener(icon: IconImageStandard) {
iconPickerViewModel.pickStandardIcon(icon)
}
override fun onIconClickListener(icon: IconImageStandard) {}
}

View File

@@ -1,6 +1,7 @@
package com.kunzisoft.keepass.adapters
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -10,8 +11,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.icon.IconImageDraw
import com.kunzisoft.keepass.icons.IconDrawableFactory
class IconAdapter<I: IconImageDraw>(val context: Context, val tintIcon: Int)
: RecyclerView.Adapter<IconAdapter<I>.CustomIconViewHolder>() {
class IconPickerAdapter<I: IconImageDraw>(val context: Context, private val tintIcon: Int)
: RecyclerView.Adapter<IconPickerAdapter<I>.CustomIconViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
@@ -23,6 +24,10 @@ class IconAdapter<I: IconImageDraw>(val context: Context, val tintIcon: Int)
val lastPosition: Int
get() = iconList.lastIndex
fun containsIcon(icon: I): Boolean {
return iconList.contains(icon)
}
fun addIcon(icon: I) {
if (!iconList.contains(icon)) {
iconList.add(icon)
@@ -30,6 +35,38 @@ class IconAdapter<I: IconImageDraw>(val context: Context, val tintIcon: Int)
}
}
fun updateIcon(icon: I) {
if (iconList.contains(icon)) {
iconList[iconList.indexOf(icon)] = icon
notifyItemChanged(iconList.indexOf(icon))
}
}
fun removeIcon(icon: I) {
if (iconList.contains(icon)) {
val position = iconList.indexOf(icon)
iconList.remove(icon)
notifyItemRemoved(position)
}
}
fun containsAnySelectedIcon(): Boolean {
return iconList.firstOrNull { it.selected } != null
}
fun deselectAllIcons() {
iconList.forEachIndexed { index, icon ->
if (icon.selected) {
icon.selected = false
notifyItemChanged(index)
}
}
}
fun getSelectedIcons(): List<I> {
return iconList.filter { it.selected }
}
fun setList(icons: List<I>) {
iconList.clear()
icons.forEach { iconImage ->
@@ -46,7 +83,14 @@ class IconAdapter<I: IconImageDraw>(val context: Context, val tintIcon: Int)
override fun onBindViewHolder(holder: CustomIconViewHolder, position: Int) {
val icon = iconList[position]
iconDrawableFactory?.assignDatabaseIcon(holder.iconImageView, icon, tintIcon)
holder.itemView.setOnClickListener { iconPickerListener?.iconPicked(icon) }
holder.itemView.setBackgroundColor(if (icon.selected) Color.RED else Color.TRANSPARENT)
holder.itemView.setOnClickListener {
iconPickerListener?.onIconClickListener(icon)
}
holder.itemView.setOnLongClickListener {
iconPickerListener?.onIconLongClickListener(icon)
true
}
}
override fun getItemCount(): Int {
@@ -54,7 +98,8 @@ class IconAdapter<I: IconImageDraw>(val context: Context, val tintIcon: Int)
}
interface IconPickerListener<I: IconImageDraw> {
fun iconPicked(icon: I)
fun onIconClickListener(icon: I)
fun onIconLongClickListener(icon: I)
}
inner class CustomIconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

View File

@@ -121,8 +121,9 @@ class Database {
return mDatabaseKDBX?.buildNewCustomIcon(cacheDirectory)
}
fun removeCustomIcon(iconUUID: UUID) {
iconsManager.removeCustomIcon(iconUUID)
fun removeCustomIcon(customIcon: IconImageCustom) {
iconDrawableFactory.clearFromCache(customIcon)
iconsManager.removeCustomIcon(customIcon.uuid)
}
val allowName: Boolean

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.element.icon
import android.os.Parcel
import android.os.Parcelable
class IconImage() : Parcelable, IconImageDraw {
class IconImage() : IconImageDraw(), Parcelable {
var standard: IconImageStandard = IconImageStandard()
var custom: IconImageCustom = IconImageCustom()

View File

@@ -1,8 +1,29 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.database.element.icon
interface IconImageDraw {
abstract class IconImageDraw {
var selected = false
/**
* Only to retrieve an icon image to Draw, to not use as object to manipulate
*/
fun getIconImageToDraw(): IconImage
abstract fun getIconImageToDraw(): IconImage
}

View File

@@ -208,6 +208,13 @@ class IconDrawableFactory(private val retrieveCipherKey : () -> Database.LoadedK
getIconDrawable(resources, icon.custom)
}
/**
* Clear a specific icon from the cache
*/
fun clearFromCache(icon: IconImageCustom) {
customIconMap.remove(icon.uuid)
}
/**
* Clear the cache of icons
*/

View File

@@ -1,69 +1,57 @@
package com.kunzisoft.keepass.viewmodels
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.icon.IconImageCustom
import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.tasks.BinaryStreamManager.resizeBitmapAndStoreDataInBinaryFile
import kotlinx.coroutines.*
import java.io.File
class IconPickerViewModel: ViewModel() {
private val mainScope = CoroutineScope(Dispatchers.Main)
val iconStandardSelected: MutableLiveData<IconImageStandard> by lazy {
val standardIconPicked: MutableLiveData<IconImageStandard> by lazy {
MutableLiveData<IconImageStandard>()
}
val iconCustomSelected: MutableLiveData<IconImageCustom> by lazy {
val customIconPicked: MutableLiveData<IconImageCustom> by lazy {
MutableLiveData<IconImageCustom>()
}
val iconCustomAdded: MutableLiveData<IconCustomState> by lazy {
val customIconsSelected: MutableLiveData<List<IconImageCustom>> by lazy {
MutableLiveData<List<IconImageCustom>>()
}
val customIconAdded: MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
fun selectIconStandard(icon: IconImageStandard) {
iconStandardSelected.value = icon
val customIconRemoved: MutableLiveData<IconCustomState> by lazy {
MutableLiveData<IconCustomState>()
}
fun selectIconCustom(icon: IconImageCustom) {
iconCustomSelected.value = icon
fun pickStandardIcon(icon: IconImageStandard) {
standardIconPicked.value = icon
}
fun addCustomIcon(database: Database,
contentResolver: ContentResolver,
iconDir: File,
iconToUploadUri: Uri) {
mainScope.launch {
withContext(Dispatchers.IO) {
// on Progress with thread
val asyncResult: Deferred<IconImageCustom?> = async {
database.buildNewCustomIcon(iconDir)?.let { customIcon ->
resizeBitmapAndStoreDataInBinaryFile(contentResolver,
iconToUploadUri, customIcon.binaryFile)
customIcon
}
}
withContext(Dispatchers.Main) {
asyncResult.await()?.let { customIcon ->
var error = false
if (customIcon.binaryFile.length <= 0) {
database.removeCustomIcon(customIcon.uuid)
error = true
}
iconCustomAdded.value = IconCustomState(customIcon, error)
}
}
}
}
fun pickCustomIcon(icon: IconImageCustom) {
customIconPicked.value = icon
}
fun selectCustomIcons(icons: List<IconImageCustom>) {
customIconsSelected.value = icons
}
fun deselectAllCustomIcons() {
customIconsSelected.value = listOf()
}
fun addCustomIcon(customIcon: IconCustomState) {
customIconAdded.value = customIcon
}
fun removeCustomIcon(customIcon: IconCustomState) {
customIconRemoved.value = customIcon
}
data class IconCustomState(val iconCustom: IconImageCustom, val error: Boolean): Parcelable {

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 Jeremy Jamet / Kunzisoft.
This file is part of KeePassDX.
KeePassDX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
KeePassDX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_delete"
android:icon="@drawable/ic_delete_forever_white_24dp"
android:title="@string/menu_delete"
android:orderInCategory="10"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -140,6 +140,7 @@
<string name="error_rebuild_list">Unable to properly rebuild the list.</string>
<string name="error_file_to_big">The file you are trying to upload is too big.</string>
<string name="error_upload_file">An error occurred while uploading the file data.</string>
<string name="error_remove_file">An error occurred while removing the file data.</string>
<string name="field_name">Field name</string>
<string name="field_value">Field value</string>
<string name="file_not_found_content">Could not find file. Try reopening it from your file browser.</string>