mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Resize image stream dynamically to show image preview #919
This commit is contained in:
@@ -3,6 +3,7 @@ KeePassDX(2.9.14)
|
|||||||
* Dark Themes #532 #714
|
* Dark Themes #532 #714
|
||||||
* Fix binary deduplication #715
|
* Fix binary deduplication #715
|
||||||
* Fix IconId #901
|
* Fix IconId #901
|
||||||
|
* Resize image stream dynamically to prevent slowdown #919
|
||||||
* Small changes #795 #900 #903 #909 #914
|
* Small changes #795 #900 #903 #909 #914
|
||||||
|
|
||||||
KeePassDX(2.9.13)
|
KeePassDX(2.9.13)
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ 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 com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryStreamManager
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class ImageViewerActivity : LockingActivity() {
|
class ImageViewerActivity : LockingActivity() {
|
||||||
|
|
||||||
@@ -51,6 +53,12 @@ class ImageViewerActivity : LockingActivity() {
|
|||||||
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)
|
val progressView: View = findViewById(R.id.image_viewer_progress)
|
||||||
|
|
||||||
|
// Approximately, to not OOM and allow a zoom
|
||||||
|
val mImagePreviewMaxWidth = max(
|
||||||
|
resources.displayMetrics.widthPixels * 2,
|
||||||
|
resources.displayMetrics.heightPixels * 2
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
progressView.visibility = View.VISIBLE
|
progressView.visibility = View.VISIBLE
|
||||||
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
intent.getParcelableExtra<Attachment>(IMAGE_ATTACHMENT_TAG)?.let { attachment ->
|
||||||
@@ -58,7 +66,11 @@ class ImageViewerActivity : LockingActivity() {
|
|||||||
supportActionBar?.title = attachment.name
|
supportActionBar?.title = attachment.name
|
||||||
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryFile.length)
|
supportActionBar?.subtitle = Formatter.formatFileSize(this, attachment.binaryFile.length)
|
||||||
|
|
||||||
Attachment.loadBitmap(attachment, Database.getInstance().loadedCipherKey) { bitmapLoaded ->
|
BinaryStreamManager.loadBitmap(
|
||||||
|
attachment.binaryFile,
|
||||||
|
Database.getInstance().loadedCipherKey,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { bitmapLoaded ->
|
||||||
if (bitmapLoaded == null) {
|
if (bitmapLoaded == null) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ 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
|
||||||
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
|
||||||
import com.kunzisoft.keepass.model.StreamDirection
|
import com.kunzisoft.keepass.model.StreamDirection
|
||||||
|
import com.kunzisoft.keepass.tasks.BinaryStreamManager
|
||||||
import com.kunzisoft.keepass.view.expand
|
import com.kunzisoft.keepass.view.expand
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
class EntryAttachmentsItemsAdapter(context: Context)
|
class EntryAttachmentsItemsAdapter(context: Context)
|
||||||
@@ -48,6 +49,11 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
var onItemClickListener: ((item: EntryAttachmentState)->Unit)? = null
|
||||||
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
var onBinaryPreviewLoaded: ((item: EntryAttachmentState) -> Unit)? = null
|
||||||
|
|
||||||
|
// Approximately
|
||||||
|
private val mImagePreviewMaxWidth = max(
|
||||||
|
context.resources.displayMetrics.widthPixels,
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.item_file_info_height)
|
||||||
|
)
|
||||||
private var mTitleColor: Int
|
private var mTitleColor: Int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -76,7 +82,11 @@ class EntryAttachmentsItemsAdapter(context: Context)
|
|||||||
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
if (entryAttachmentState.previewState == AttachmentState.NULL) {
|
||||||
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
entryAttachmentState.previewState = AttachmentState.IN_PROGRESS
|
||||||
// Load the bitmap image
|
// Load the bitmap image
|
||||||
Attachment.loadBitmap(entryAttachmentState.attachment, binaryCipherKey) { imageLoaded ->
|
BinaryStreamManager.loadBitmap(
|
||||||
|
entryAttachmentState.attachment.binaryFile,
|
||||||
|
binaryCipherKey,
|
||||||
|
mImagePreviewMaxWidth
|
||||||
|
) { imageLoaded ->
|
||||||
if (imageLoaded == null) {
|
if (imageLoaded == null) {
|
||||||
entryAttachmentState.previewState = AttachmentState.ERROR
|
entryAttachmentState.previewState = AttachmentState.ERROR
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
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.BinaryFile
|
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
data class Attachment(var name: String,
|
data class Attachment(var name: String,
|
||||||
var binaryFile: BinaryFile) : Parcelable {
|
var binaryFile: BinaryFile) : Parcelable {
|
||||||
@@ -68,28 +66,5 @@ 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 ->
|
|
||||||
var bitmap: Bitmap?
|
|
||||||
attachment.binaryFile.getUnGzipInputDataStream(binaryKey).use { bitmapInputStream ->
|
|
||||||
bitmap = BitmapFactory.decodeStream(bitmapInputStream)
|
|
||||||
}
|
|
||||||
bitmap
|
|
||||||
}
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
actionOnFinish(asyncResult.await())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,15 @@ import com.kunzisoft.keepass.database.element.Database
|
|||||||
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
import com.kunzisoft.keepass.database.element.database.BinaryFile
|
||||||
import com.kunzisoft.keepass.stream.readAllBytes
|
import com.kunzisoft.keepass.stream.readAllBytes
|
||||||
import com.kunzisoft.keepass.utils.UriUtil
|
import com.kunzisoft.keepass.utils.UriUtil
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.ln
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
object BinaryStreamManager {
|
object BinaryStreamManager {
|
||||||
|
|
||||||
@@ -126,6 +131,57 @@ object BinaryStreamManager {
|
|||||||
return Bitmap.createScaledBitmap(this, width, height, true)
|
return Bitmap.createScaledBitmap(this, width, height, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadBitmap(binaryFile: BinaryFile,
|
||||||
|
binaryCipherKey: Database.LoadedKey?,
|
||||||
|
maxWidth: Int,
|
||||||
|
actionOnFinish: (Bitmap?) -> Unit) {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val asyncResult: Deferred<Bitmap?> = async {
|
||||||
|
runCatching {
|
||||||
|
binaryCipherKey?.let { binaryKey ->
|
||||||
|
val bitmap: Bitmap? = decodeSampledBitmap(binaryFile,
|
||||||
|
binaryKey,
|
||||||
|
maxWidth)
|
||||||
|
bitmap
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
actionOnFinish(asyncResult.await())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeSampledBitmap(binaryFile: BinaryFile,
|
||||||
|
binaryCipherKey: Database.LoadedKey,
|
||||||
|
maxWidth: Int): Bitmap? {
|
||||||
|
// First decode with inJustDecodeBounds=true to check dimensions
|
||||||
|
return BitmapFactory.Options().run {
|
||||||
|
try {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
binaryFile.getUnGzipInputDataStream(binaryCipherKey).use {
|
||||||
|
BitmapFactory.decodeStream(it, null, this)
|
||||||
|
}
|
||||||
|
// Calculate inSampleSize
|
||||||
|
var scale = 1
|
||||||
|
if (outHeight > maxWidth || outWidth > maxWidth) {
|
||||||
|
scale = 2.0.pow(ceil(ln(maxWidth / max(outHeight, outWidth).toDouble()) / ln(0.5))).toInt()
|
||||||
|
}
|
||||||
|
inSampleSize = scale
|
||||||
|
|
||||||
|
// Decode bitmap with inSampleSize set
|
||||||
|
inJustDecodeBounds = false
|
||||||
|
binaryFile.getUnGzipInputDataStream(binaryCipherKey).use {
|
||||||
|
BitmapFactory.decodeStream(it, null, this)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const val DEFAULT_ICON_WIDTH = 64
|
private const val DEFAULT_ICON_WIDTH = 64
|
||||||
|
|
||||||
private val TAG = BinaryStreamManager::class.java.name
|
private val TAG = BinaryStreamManager::class.java.name
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<androidx.appcompat.widget.AppCompatImageView
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
android:id="@+id/item_attachment_thumbnail"
|
android:id="@+id/item_attachment_thumbnail"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="144dp"
|
android:layout_height="@dimen/image_preview_height"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<dimen name="default_margin">18dp</dimen>
|
<dimen name="default_margin">18dp</dimen>
|
||||||
<dimen name="image_button_margin">8dp</dimen>
|
<dimen name="image_button_margin">8dp</dimen>
|
||||||
<dimen name="image_list_margin">12dp</dimen>
|
<dimen name="image_list_margin">12dp</dimen>
|
||||||
|
<dimen name="image_preview_height">144dp</dimen>
|
||||||
<dimen name="button_margin">6dp</dimen>
|
<dimen name="button_margin">6dp</dimen>
|
||||||
<dimen name="card_view_margin">12dp</dimen>
|
<dimen name="card_view_margin">12dp</dimen>
|
||||||
<dimen name="card_view_padding">14dp</dimen>
|
<dimen name="card_view_padding">14dp</dimen>
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
* Dark Themes #532 #714
|
* Dark Themes #532 #714
|
||||||
* Fix binary deduplication #715
|
* Fix binary deduplication #715
|
||||||
* Fix IconId #901
|
* Fix IconId #901
|
||||||
|
* Resize image stream dynamically to prevent slowdown #919
|
||||||
* Small changes #795 #900 #903 #909 #914
|
* Small changes #795 #900 #903 #909 #914
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
* Thèmes sombres #532 #714
|
* Thèmes sombres #532 #714
|
||||||
* Correction de la duplication des binaires #715
|
* Correction de la duplication des binaires #715
|
||||||
* Correction IconId #901
|
* Correction IconId #901
|
||||||
|
* Changement de taille dynamique du flux d'image pour prévenir les ralentissements #919
|
||||||
* Petits changements #795 #900 #903 #909 #914
|
* Petits changements #795 #900 #903 #909 #914
|
||||||
Reference in New Issue
Block a user