Add thread to load images

This commit is contained in:
J-Jamet
2021-02-08 13:42:04 +01:00
parent 064c468e62
commit 8752f92cea
11 changed files with 103 additions and 32 deletions

View File

@@ -126,6 +126,7 @@ class EntryActivity : LockingActivity() {
historyView = findViewById(R.id.history_container) historyView = findViewById(R.id.history_container)
entryContentsView = findViewById(R.id.entry_contents) entryContentsView = findViewById(R.id.entry_contents)
entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this)) entryContentsView?.applyFontVisibilityToFields(PreferencesUtil.fieldFontIsInVisibility(this))
entryContentsView?.setAttachmentCipherKey(mDatabase?.loadedCipherKey)
entryProgress = findViewById(R.id.entry_progress) entryProgress = findViewById(R.id.entry_progress)
lockView = findViewById(R.id.lock_button) lockView = findViewById(R.id.lock_button)

View File

@@ -198,7 +198,7 @@ class EntryEditActivity : LockingActivity(),
// Build fragment to manage entry modification // Build fragment to manage entry modification
entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment? entryEditFragment = supportFragmentManager.findFragmentByTag(ENTRY_EDIT_FRAGMENT_TAG) as? EntryEditFragment?
if (entryEditFragment == null) { if (entryEditFragment == null) {
entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo) entryEditFragment = EntryEditFragment.getInstance(tempEntryInfo, mDatabase?.loadedCipherKey)
} }
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG) .replace(R.id.entry_edit_contents, entryEditFragment!!, ENTRY_EDIT_FRAGMENT_TAG)

View File

@@ -41,6 +41,7 @@ import com.kunzisoft.keepass.activities.lock.resetAppTimeoutWhenViewFocusedOrCha
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.icon.IconImage import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.education.EntryEditActivityEducation import com.kunzisoft.keepass.education.EntryEditActivityEducation
@@ -86,6 +87,7 @@ class EntryEditFragment: StylishFragment() {
// Elements to modify the current entry // Elements to modify the current entry
private var mEntryInfo = EntryInfo() private var mEntryInfo = EntryInfo()
private var mBinaryCipherKey: Database.LoadedKey? = null
private var mLastFocusedEditField: FocusedEditField? = null private var mLastFocusedEditField: FocusedEditField? = null
private var mExtraViewToRequestFocus: EditText? = null private var mExtraViewToRequestFocus: EditText? = null
@@ -127,6 +129,7 @@ class EntryEditFragment: StylishFragment() {
attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container) attachmentsContainerView = rootView.findViewById(R.id.entry_attachments_container)
attachmentsListView = rootView.findViewById(R.id.entry_attachments_list) attachmentsListView = rootView.findViewById(R.id.entry_attachments_list)
attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext()) attachmentsAdapter = EntryAttachmentsItemsAdapter(requireContext())
attachmentsAdapter.binaryCipherKey = arguments?.getSerializable(KEY_BINARY_CIPHER_KEY) as? Database.LoadedKey?
attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize -> attachmentsAdapter.onListSizeChangedListener = { previousSize, newSize ->
if (previousSize > 0 && newSize == 0) { if (previousSize > 0 && newSize == 0) {
attachmentsContainerView.collapse(true) attachmentsContainerView.collapse(true)
@@ -528,6 +531,7 @@ class EntryEditFragment: StylishFragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
populateEntryWithViews() populateEntryWithViews()
outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo) outState.putParcelable(KEY_TEMP_ENTRY_INFO, mEntryInfo)
outState.putSerializable(KEY_BINARY_CIPHER_KEY, mBinaryCipherKey)
outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField) outState.putParcelable(KEY_LAST_FOCUSED_FIELD, mLastFocusedEditField)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@@ -535,12 +539,15 @@ class EntryEditFragment: StylishFragment() {
companion object { companion object {
const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO" const val KEY_TEMP_ENTRY_INFO = "KEY_TEMP_ENTRY_INFO"
const val KEY_BINARY_CIPHER_KEY = "KEY_BINARY_CIPHER_KEY"
const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD" const val KEY_LAST_FOCUSED_FIELD = "KEY_LAST_FOCUSED_FIELD"
fun getInstance(entryInfo: EntryInfo?): EntryEditFragment { fun getInstance(entryInfo: EntryInfo?,
loadedKey: Database.LoadedKey?): EntryEditFragment {
return EntryEditFragment().apply { return EntryEditFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo) putParcelable(KEY_TEMP_ENTRY_INFO, entryInfo)
putSerializable(KEY_BINARY_CIPHER_KEY, loadedKey)
} }
} }
} }

View File

@@ -21,14 +21,15 @@ package com.kunzisoft.keepass.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import com.igreenwood.loupe.Loupe import com.igreenwood.loupe.Loupe
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.lock.LockingActivity import com.kunzisoft.keepass.activities.lock.LockingActivity
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import kotlinx.android.synthetic.main.activity_image_viewer.* import kotlinx.android.synthetic.main.activity_image_viewer.*
class ImageViewerActivity : LockingActivity() { class ImageViewerActivity : LockingActivity() {
@@ -38,13 +39,20 @@ class ImageViewerActivity : LockingActivity() {
setContentView(R.layout.activity_image_viewer) setContentView(R.layout.activity_image_viewer)
val imageView: ImageView = findViewById(R.id.image_viewer_image) val imageView: ImageView = findViewById(R.id.image_viewer_image)
val progressView: View = findViewById(R.id.image_viewer_progress)
try { try {
progressView.visibility = View.VISIBLE
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment -> intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
BitmapFactory.decodeStream(attachment.binaryAttachment.getUnGzipInputDataStream())?.let { imageBitmap -> Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { bitmapLoaded ->
imageView.setImageBitmap(imageBitmap) if (bitmapLoaded == null) {
} ?: finish() finish()
} } else {
progressView.visibility = View.GONE
imageView.setImageBitmap(bitmapLoaded)
}
}
} ?: finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to view the binary", e) Log.e(TAG, "Unable to view the binary", e)
finish() finish()

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.adapters
import android.content.Context import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.BitmapFactory import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.text.format.Formatter import android.text.format.Formatter
import android.util.TypedValue import android.util.TypedValue
@@ -33,6 +33,8 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.ImageViewerActivity import com.kunzisoft.keepass.activities.ImageViewerActivity
import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.model.AttachmentState import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState import com.kunzisoft.keepass.model.EntryAttachmentState
@@ -42,8 +44,12 @@ import com.kunzisoft.keepass.model.StreamDirection
class EntryAttachmentsItemsAdapter(context: Context) class EntryAttachmentsItemsAdapter(context: Context)
: AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) { : AnimatedItemsAdapter<EntryAttachmentState, EntryAttachmentsItemsAdapter.EntryBinariesViewHolder>(context) {
var binaryCipherKey: Database.LoadedKey? = null
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
// To manage image preview loading
private val imageBinariesSet = HashMap<Int, Bitmap?>()
private var mTitleColor: Int private var mTitleColor: Int
init { init {
@@ -65,14 +71,27 @@ class EntryAttachmentsItemsAdapter(context: Context)
holder.itemView.visibility = View.VISIBLE holder.itemView.visibility = View.VISIBLE
holder.binaryFileThumbnail.apply { holder.binaryFileThumbnail.apply {
BitmapFactory.decodeStream(entryAttachmentState.attachment.binaryAttachment.getUnGzipInputDataStream())?.let { imageBitmap -> // Show the bitmap image if loaded
setImageBitmap(imageBitmap) if (imageBinariesSet.containsKey(position)) {
setOnClickListener { if (imageBinariesSet[position] != null) {
ImageViewerActivity.getInstance(context, entryAttachmentState.attachment) setImageBitmap(imageBinariesSet[position])
} }
visibility = View.VISIBLE } else {
} ?: run { imageBinariesSet[position] = null
visibility = View.GONE // Load the bitmap image
Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { imageLoaded ->
imageBinariesSet[position] = imageLoaded
notifyItemChanged(position)
}
}
setOnClickListener {
ImageViewerActivity.getInstance(context, entryAttachmentState.attachment)
}
visibility = if (imageBinariesSet.containsKey(position)) {
setImageBitmap(imageBinariesSet[position])
View.VISIBLE
} else {
View.GONE
} }
} }
holder.binaryFileBroken.apply { holder.binaryFileBroken.apply {

View File

@@ -19,9 +19,12 @@
*/ */
package com.kunzisoft.keepass.database.element package com.kunzisoft.keepass.database.element
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.kunzisoft.keepass.database.element.database.BinaryAttachment import com.kunzisoft.keepass.database.element.database.BinaryAttachment
import kotlinx.coroutines.*
data class Attachment(var name: String, data class Attachment(var name: String,
var binaryAttachment: BinaryAttachment) : Parcelable { var binaryAttachment: BinaryAttachment) : Parcelable {
@@ -65,5 +68,25 @@ data class Attachment(var name: String,
override fun newArray(size: Int): Array<Attachment?> { override fun newArray(size: Int): Array<Attachment?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
fun loadBitmap(attachment: Attachment,
binaryCipherKey: Database.LoadedKey?,
actionOnFinish: (Bitmap?) -> Unit) {
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
val asyncResult: Deferred<Bitmap?> = async {
runCatching {
binaryCipherKey?.let { binaryKey ->
BitmapFactory.decodeStream(attachment.binaryAttachment
.getUnGzipInputDataStream(binaryKey))
}
}.getOrNull()
}
withContext(Dispatchers.Main) {
actionOnFinish(asyncResult.await())
}
}
}
}
} }
} }

View File

@@ -346,14 +346,14 @@ class Database {
this.loaded = true this.loaded = true
} }
class LoadedKey(val key: Key, val iv: IvParameterSpec) { class LoadedKey(val key: Key, val iv: ByteArray): Serializable {
companion object { companion object {
const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding" const val BINARY_CIPHER = "Blowfish/CBC/PKCS5Padding"
fun generateNewCipherKey(): LoadedKey { fun generateNewCipherKey(): LoadedKey {
val iv = ByteArray(8) val iv = ByteArray(8)
SecureRandom().nextBytes(iv) SecureRandom().nextBytes(iv)
return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), IvParameterSpec(iv)) return LoadedKey(KeyGenerator.getInstance("Blowfish").generateKey(), iv)
} }
} }
} }

View File

@@ -32,6 +32,7 @@ import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
class BinaryAttachment : Parcelable { class BinaryAttachment : Parcelable {
@@ -101,7 +102,7 @@ class BinaryAttachment : Parcelable {
private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream { private fun buildInputStream(file: File?, cipherKey: Database.LoadedKey): InputStream {
return when { return when {
file != null && file.length() > 0 -> { file != null && file.length() > 0 -> {
cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, cipherKey.iv) cipherDecryption.init(Cipher.DECRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP) Base64InputStream(CipherInputStream(FileInputStream(file), cipherDecryption), Base64.NO_WRAP)
} }
else -> ByteArrayInputStream(ByteArray(0)) else -> ByteArrayInputStream(ByteArray(0))
@@ -112,7 +113,7 @@ class BinaryAttachment : Parcelable {
private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream { private fun buildOutputStream(file: File?, cipherKey: Database.LoadedKey): OutputStream {
return when { return when {
file != null -> { file != null -> {
cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, cipherKey.iv) cipherEncryption.init(Cipher.ENCRYPT_MODE, cipherKey.key, IvParameterSpec(cipherKey.iv))
BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP)) BinaryCountingOutputStream(Base64OutputStream(CipherOutputStream(FileOutputStream(file), cipherEncryption), Base64.NO_WRAP))
} }
else -> throw IOException("Unable to write in an unknown file") else -> throw IOException("Unable to write in an unknown file")

View File

@@ -35,6 +35,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter import com.kunzisoft.keepass.adapters.EntryAttachmentsItemsAdapter
import com.kunzisoft.keepass.adapters.EntryHistoryAdapter import com.kunzisoft.keepass.adapters.EntryHistoryAdapter
import com.kunzisoft.keepass.database.element.Attachment import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.DateInstant import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
@@ -321,6 +322,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
* ------------- * -------------
*/ */
fun setAttachmentCipherKey(cipherKey: Database.LoadedKey?) {
attachmentsAdapter.binaryCipherKey = cipherKey
}
private fun showAttachments(show: Boolean) { private fun showAttachments(show: Boolean) {
attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE attachmentsContainerView.visibility = if (show) View.VISIBLE else View.GONE
} }

View File

@@ -5,6 +5,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?android:attr/windowBackground"> android:background="?android:attr/windowBackground">
<ProgressBar
android:id="@+id/image_viewer_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<ImageView <ImageView
android:id="@+id/image_viewer_image" android:id="@+id/image_viewer_image"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -31,17 +31,7 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout" android:id="@+id/constraintLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:layout_alignParentBottom="true">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_thumbnail"
android:layout_width="72dp"
android:layout_height="72dp"
android:src="@drawable/material_00_32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_broken" android:id="@+id/item_attachment_broken"
@@ -50,7 +40,7 @@
android:contentDescription="@string/entry_attachments" android:contentDescription="@string/entry_attachments"
android:src="@drawable/ic_attach_file_broken_white_24dp" android:src="@drawable/ic_attach_file_broken_white_24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/item_attachment_thumbnail" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
@@ -59,11 +49,23 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintStart_toEndOf="@+id/item_attachment_broken" app:layout_constraintStart_toEndOf="@+id/item_attachment_broken"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_thumbnail"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="BinaryFile.attach" /> tools:text="BinaryFile.attach" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/item_attachment_thumbnail"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/item_attachment_size_container"
app:layout_constraintStart_toEndOf="@+id/item_attachment_title"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <LinearLayout
android:id="@+id/item_attachment_size_container" android:id="@+id/item_attachment_size_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"