Skip to content
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
55a6929
Thumbail Generation study - inconsistent changes
JustFanta01 Apr 11, 2024
f1ca280
Added thumbnail File in CryptoFile and logic for storing and retrievi…
JustFanta01 Apr 13, 2024
ef39584
Remove thumbnail from cache, add check if thumbnail exists only for i…
taglioIsCoding Apr 13, 2024
53779c4
Generate thumbnails only for images
taglioIsCoding Apr 14, 2024
e69bb98
Removed unused imports
taglioIsCoding Apr 19, 2024
ce1c989
Add local LRU cache and first refactor
taglioIsCoding Apr 21, 2024
b30afb1
Removed unused imports + fix typo
JustFanta01 Apr 21, 2024
e0fa1be
changed names for retrieving cloud-related DiskLruCache
JustFanta01 Apr 28, 2024
b31de8e
modified the cachekey for DiskLruCache
JustFanta01 Apr 28, 2024
e20d516
Added also in FormatPre7 the fetch of the thumbnail (not tested yet)
JustFanta01 Apr 28, 2024
ca57efb
Add enum Thumbnail option
taglioIsCoding Apr 29, 2024
041b2d8
minor changes
JustFanta01 Apr 29, 2024
abcc3e2
Change cachekey for thumbnails
JustFanta01 May 7, 2024
d02e811
Use onEach and added the delete operation in the FormatPre7
JustFanta01 May 7, 2024
6daefd7
Added a separate thread to acquire the bitmap of the image and genera…
JustFanta01 Apr 28, 2024
4d0e715
Add thumbanil generator thread pool executor and subsample image stream
JustFanta01 May 1, 2024
fe0151d
Cleanup
taglioIsCoding May 8, 2024
00f766f
Manage rename and move files in cache
taglioIsCoding May 8, 2024
68b8744
Rebase and PR minor changes
JustFanta01 Sep 6, 2024
7110a54
Refactor thumbnail association
JustFanta01 Sep 7, 2024
e05f206
Force thumbnail generation to compare listing performance
JustFanta01 Sep 9, 2024
64ba7cf
Fix mistake
JustFanta01 Sep 10, 2024
911c196
Merge branch 'cryptomator:develop' into feature/thumbnail-playground
JustFanta01 Oct 8, 2024
c25714d
Add multithreaded parallelism in the thumbnail generation
JustFanta01 Sep 24, 2024
82525c0
Add automatic download and generation of thumbnails after scroll and …
JustFanta01 Oct 6, 2024
034a29b
Add AssociateThumbanilUseCase to fetch thumbnails from cache not in M…
JustFanta01 Oct 6, 2024
80c013e
Unify the readGenerateThumbnail() with the original read() and minor …
JustFanta01 Oct 6, 2024
932d7d2
Change thumbnail cachekey, add ThumbnailOption.READONLY and automatic…
JustFanta01 Oct 8, 2024
d85096b
Fix thumbnail settings and clean up
JustFanta01 Oct 12, 2024
a45edea
Minor fixes :)
JustFanta01 Oct 14, 2024
bdc28c2
Merge branch 'cryptomator:develop' into feature/thumbnail-playground
JustFanta01 Jan 20, 2025
340338a
Merge branch 'develop' into feature/thumbnail-playground
JustFanta01 May 5, 2025
24c9747
UI hardening
JustFanta01 May 5, 2025
9ecfcda
Fix large image handling and thumbnail generation for 30MB+ files
aiya000 Aug 5, 2025
f6c608e
Fix thumbnail display issues in folder view
aiya000 Aug 5, 2025
861f0be
Merge pull request #1 from aiya000/fix/feature-thumbnail-generation-e…
aiya000 Aug 5, 2025
2271139
Merge pull request #8 from aiya000/fix/generate-thumbnails-for-larger…
WheelyMcBones Oct 19, 2025
8ac93f4
addressed review, enhanced thumbnail generation error handling for un…
WheelyMcBones Oct 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.cryptomator.domain.repository.CloudContentRepository
import org.cryptomator.domain.usecases.ProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.FileTransferState
import org.cryptomator.domain.usecases.cloud.UploadState
import java.io.File
import java.io.OutputStream
Expand Down Expand Up @@ -95,6 +96,11 @@ internal class CryptoCloudContentRepository(context: Context, cloudContentReposi
cryptoImpl.read(file, data, progressAware)
}

@Throws(BackendException::class)
override fun associateThumbnails(list: List<CryptoNode>, progressAware: ProgressAware<FileTransferState>) {
cryptoImpl.associateThumbnails(list, progressAware)
}

@Throws(BackendException::class)
override fun delete(node: CryptoNode) {
cryptoImpl.delete(node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.cryptomator.data.cloud.crypto

import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import java.io.File
import java.util.Date

class CryptoFile(
Expand All @@ -12,6 +13,8 @@ class CryptoFile(
val cloudFile: CloudFile
) : CloudFile, CryptoNode {

var thumbnail : File? = null

override val cloud: Cloud?
get() = parent.cloud

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package org.cryptomator.data.cloud.crypto

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.ThumbnailUtils
import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.tomclaw.cache.DiskLruCache
import org.cryptomator.cryptolib.api.Cryptor
import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel
Expand All @@ -9,6 +14,7 @@ import org.cryptomator.domain.Cloud
import org.cryptomator.domain.CloudFile
import org.cryptomator.domain.CloudFolder
import org.cryptomator.domain.CloudNode
import org.cryptomator.domain.CloudType
import org.cryptomator.domain.exception.BackendException
import org.cryptomator.domain.exception.CloudNodeAlreadyExistsException
import org.cryptomator.domain.exception.EmptyDirFileException
Expand All @@ -22,20 +28,36 @@ import org.cryptomator.domain.usecases.UploadFileReplacingProgressAware
import org.cryptomator.domain.usecases.cloud.DataSource
import org.cryptomator.domain.usecases.cloud.DownloadState
import org.cryptomator.domain.usecases.cloud.FileBasedDataSource.Companion.from
import org.cryptomator.domain.usecases.cloud.FileTransferState
import org.cryptomator.domain.usecases.cloud.Progress
import org.cryptomator.domain.usecases.cloud.UploadState
import org.cryptomator.util.SharedPreferencesHandler
import org.cryptomator.util.ThumbnailsOption
import org.cryptomator.util.file.LruFileCacheUtil
import org.cryptomator.util.file.MimeType
import org.cryptomator.util.file.MimeTypeMap
import org.cryptomator.util.file.MimeTypes
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.util.LinkedList
import java.util.Queue
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.function.Supplier
import kotlin.system.measureTimeMillis
import timber.log.Timber


abstract class CryptoImplDecorator(
Expand All @@ -50,6 +72,59 @@ abstract class CryptoImplDecorator(
@Volatile
private var root: RootCryptoFolder? = null

private val sharedPreferencesHandler = SharedPreferencesHandler(context)

private var diskLruCache: MutableMap<LruFileCacheUtil.Cache, DiskLruCache?> = mutableMapOf()

private val mimeTypes = MimeTypes(MimeTypeMap())

private val thumbnailExecutorService: ExecutorService by lazy {
val threadFactory = ThreadFactoryBuilder().setNameFormat("thumbnail-generation-thread-%d").build()
Executors.newCachedThreadPool(threadFactory)
}

protected fun getLruCacheFor(type: CloudType): DiskLruCache? {
return getOrCreateLruCache(getCacheTypeFromCloudType(type), sharedPreferencesHandler.lruCacheSize())
}

private fun getOrCreateLruCache(cache: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? {
return diskLruCache.computeIfAbsent(cache) {
val cacheFile = LruFileCacheUtil(context).resolve(it)
try {
DiskLruCache.create(cacheFile, cacheSize.toLong())
} catch (e: IOException) {
Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $cacheFile.name")
null
}
}
}

protected fun renameFileInCache(source: CryptoFile, target: CryptoFile) {
val oldCacheKey = generateCacheKey(source)
val newCacheKey = generateCacheKey(target)
source.cloudFile.cloud?.type()?.let { cloudType ->
getLruCacheFor(cloudType)?.let { diskCache ->
if (diskCache[oldCacheKey] != null) {
target.thumbnail = diskCache.put(newCacheKey, diskCache[oldCacheKey])
diskCache.delete(oldCacheKey)
}
}
}
}

private fun getCacheTypeFromCloudType(type: CloudType): LruFileCacheUtil.Cache {
return when (type) {
CloudType.DROPBOX -> LruFileCacheUtil.Cache.DROPBOX
CloudType.GOOGLE_DRIVE -> LruFileCacheUtil.Cache.GOOGLE_DRIVE
CloudType.ONEDRIVE -> LruFileCacheUtil.Cache.ONEDRIVE
CloudType.PCLOUD -> LruFileCacheUtil.Cache.PCLOUD
CloudType.WEBDAV -> LruFileCacheUtil.Cache.WEBDAV
CloudType.S3 -> LruFileCacheUtil.Cache.S3
CloudType.LOCAL -> LruFileCacheUtil.Cache.LOCAL
else -> throw IllegalStateException("Unexpected CloudType: $type")
}
}

@Throws(BackendException::class)
abstract fun folder(cryptoParent: CryptoFolder, cleartextName: String): CryptoFolder

Expand Down Expand Up @@ -309,8 +384,22 @@ abstract class CryptoImplDecorator(
@Throws(BackendException::class)
fun read(cryptoFile: CryptoFile, data: OutputStream, progressAware: ProgressAware<DownloadState>) {
val ciphertextFile = cryptoFile.cloudFile

val diskCache = cryptoFile.cloudFile.cloud?.type()?.let { getLruCacheFor(it) }
val cacheKey = generateCacheKey(cryptoFile)
val genThumbnail = isThumbnailGenerationAvailable(diskCache, cryptoFile.name)
var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null)

val thumbnailWriter = PipedOutputStream()
val thumbnailReader = PipedInputStream(thumbnailWriter)

try {
val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)

if (genThumbnail) {
futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader)
}
Comment on lines +391 to +401
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t truncate downloads on thumbnail pipe errors; lazy-create pipes and confine errors to the thumbnail path.

Currently, an IOException “Pipe closed” bubbles to the outer catch, causing an early return from read() and a truncated file. Create the pipe only when needed and handle pipe write failures inline so decryption continues.

-    var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null)
-
-    val thumbnailWriter = PipedOutputStream()
-    val thumbnailReader = PipedInputStream(thumbnailWriter)
+    var futureThumbnail: Future<*> = CompletableFuture.completedFuture(null)
+    var thumbnailWriter: PipedOutputStream? = null
+    var thumbnailReader: PipedInputStream? = null
+    var thumbnailPipeOpen = false
-    val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)
-
-    if (genThumbnail) {
-        futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader)
-    }
+    val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)
+    if (genThumbnail) {
+        thumbnailWriter = PipedOutputStream()
+        thumbnailReader = PipedInputStream(thumbnailWriter)
+        futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader!!)
+        thumbnailPipeOpen = true
+    }
-                        data.write(buff.array(), 0, buff.remaining())
-                        if (genThumbnail) {
-                            thumbnailWriter.write(buff.array(), 0, buff.remaining())
-                        }
+                        data.write(buff.array(), 0, buff.remaining())
+                        if (thumbnailPipeOpen) {
+                            try {
+                                thumbnailWriter!!.write(buff.array(), 0, buff.remaining())
+                            } catch (ioe: IOException) {
+                                if (ioe.message?.contains("Pipe closed") == true) {
+                                    Timber.d("Thumbnail pipe closed (continuing without thumbnail): ${cryptoFile.name}")
+                                    closeQuietly(thumbnailWriter!!)
+                                    thumbnailPipeOpen = false
+                                } else {
+                                    throw ioe
+                                }
+                            }
+                        }
-                }
-            } finally {
+                }
+            } finally {
                 encryptedTmpFile.delete()
                 progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)))
             }
 
-            // Close thumbnail writer first, then wait for thumbnail generation to complete
-            if (genThumbnail) {
-                closeQuietly(thumbnailWriter)
-                try {
-                    futureThumbnail.get(5, java.util.concurrent.TimeUnit.SECONDS) // Add timeout to prevent hanging
+            // Close thumbnail writer first, then wait for thumbnail generation to complete
+            if (thumbnailPipeOpen) {
+                closeQuietly(thumbnailWriter!!)
+                try {
+                    futureThumbnail.get(5, java.util.concurrent.TimeUnit.SECONDS)
                 } catch (e: java.util.concurrent.TimeoutException) {
                     Timber.w("Thumbnail generation timed out for ${cryptoFile.name}")
                     futureThumbnail.cancel(true)
                 } catch (e: Exception) {
                     Timber.w(e, "Error waiting for thumbnail generation for ${cryptoFile.name}")
                 }
             }
-            closeQuietly(thumbnailReader)
+            thumbnailReader?.let { closeQuietly(it) }
         } catch (e: IOException) {
-            // Don't treat thumbnail-related pipe closed errors as fatal
-            if (e.message?.contains("Pipe closed") == true && genThumbnail) {
-                Timber.d("Pipe closed during thumbnail generation (expected): ${cryptoFile.name}")
-                // The file was successfully decrypted, just the thumbnail failed
-                return
-            }
             throw FatalBackendException(e)
         }

This keeps decryption intact even if thumbnail generation aborts.

Also applies to: 414-417, 429-449, 450-456

🤖 Prompt for AI Agents
In data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
around lines 391-401 (and also apply similar changes at 414-417, 429-449,
450-456): currently the PipedOutputStream/PipedInputStream are created
unconditionally and IOExceptions from the pipe can bubble out and abort
decryption; change to lazily create the pipe only when genThumbnail is true and
the thumbnail thread will be started, start the thumbnail thread after creating
the streams, and wrap any writes to the pipe in a local try/catch that catches
IOExceptions (e.g. "Pipe closed") so they are handled inline—close/cleanup the
pipe and suppress the exception so it does not propagate to the outer catch—thus
confining pipe errors to the thumbnail path and allowing decryption to continue
uninterrupted.


progressAware.onProgress(Progress.started(DownloadState.decryption(cryptoFile)))
try {
Channels.newChannel(FileInputStream(encryptedTmpFile)).use { readableByteChannel ->
Expand All @@ -322,7 +411,12 @@ abstract class CryptoImplDecorator(
while (decryptingReadableByteChannel.read(buff).also { read = it } > 0) {
buff.flip()
data.write(buff.array(), 0, buff.remaining())
if (genThumbnail) {
thumbnailWriter.write(buff.array(), 0, buff.remaining())
}

decrypted += read.toLong()

progressAware
.onProgress(
Progress.progress(DownloadState.decryption(cryptoFile)) //
Expand All @@ -332,16 +426,178 @@ abstract class CryptoImplDecorator(
)
}
}
thumbnailWriter.flush()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Flush should be conditional to avoid NPE with nullable pipes.

The flush is called unconditionally, but if pipes are made nullable (as suggested earlier), this will cause a NullPointerException when genThumbnail is false.

Apply this diff:

-					thumbnailWriter.flush()
+					if (genThumbnail) {
+						thumbnailWriter?.flush()
+					}

Or better, move it into the finalization block at lines 441-453 where thumbnail operations are cleaned up.

🤖 Prompt for AI Agents
In data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
around line 433, thumbnailWriter.flush() is called unconditionally which will
throw an NPE if pipes are nullable and genThumbnail is false; move the flush
call into the existing thumbnail finalization block at lines ~441-453 (where
thumbnail streams/flags are cleaned up) and additionally guard it with a null
check (e.g., if (thumbnailWriter != null) thumbnailWriter.flush()) or wrap it in
the same genThumbnail condition so flush only executes when thumbnailWriter was
actually created.

}
} finally {
encryptedTmpFile.delete()
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)))
}

// Close thumbnail writer first, then wait for thumbnail generation to complete
if (genThumbnail) {
closeQuietly(thumbnailWriter)
try {
futureThumbnail.get(5, java.util.concurrent.TimeUnit.SECONDS) // Add timeout to prevent hanging
} catch (e: java.util.concurrent.TimeoutException) {
Timber.w("Thumbnail generation timed out for ${cryptoFile.name}")
futureThumbnail.cancel(true)
} catch (e: Exception) {
Timber.w(e, "Error waiting for thumbnail generation for ${cryptoFile.name}")
}
}
closeQuietly(thumbnailReader)
} catch (e: IOException) {
// Don't treat thumbnail-related pipe closed errors as fatal
if (e.message?.contains("Pipe closed") == true && genThumbnail) {
Timber.d("Pipe closed during thumbnail generation (expected): ${cryptoFile.name}")
// The file was successfully decrypted, just the thumbnail failed
return
}
throw FatalBackendException(e)
}
}

private fun closeQuietly(closeable: Closeable) {
try {
closeable.close();
} catch (e: IOException) {
// ignore
}
}

private fun startThumbnailGeneratorThread(cryptoFile: CryptoFile, diskCache: DiskLruCache, cacheKey: String, thumbnailReader: PipedInputStream): Future<*> {
return thumbnailExecutorService.submit {
try {
val options = BitmapFactory.Options()
val thumbnailBitmap: Bitmap?

// Use aggressive sampling for memory efficiency with large images
// Estimate file size and adjust sample size accordingly
val fileSize = cryptoFile.size ?: 0L
val fileSizeMB = fileSize / (1024 * 1024)

// Calculate sample size based on file size to prevent OOM
var sampleSize = when {
fileSizeMB > 50 -> 16 // 1/256 of original size for very large files
fileSizeMB > 30 -> 12 // 1/144 of original size for large files
fileSizeMB > 20 -> 8 // 1/64 of original size for medium-large files
fileSizeMB > 10 -> 6 // 1/36 of original size for medium files
else -> 4 // 1/16 of original size for smaller files
}

options.inSampleSize = sampleSize
options.inPreferredConfig = Bitmap.Config.RGB_565 // Use less memory than ARGB_8888
options.inDither = false
options.inPurgeable = true // Allow system to purge bitmap from memory if needed
options.inInputShareable = true

Timber.d("Generating thumbnail for ${cryptoFile.name} (${fileSizeMB}MB) with sampleSize: $sampleSize")

val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options)
if (bitmap == null) {
closeQuietly(thumbnailReader)
Timber.w("Failed to decode bitmap for thumbnail generation: ${cryptoFile.name}")
return@submit
}

val thumbnailWidth = 100
val thumbnailHeight = 100
thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight)

// Clean up the original bitmap to free memory immediately
if (bitmap != thumbnailBitmap) {
bitmap.recycle()
}

if (thumbnailBitmap != null) {
storeThumbnail(diskCache, cacheKey, thumbnailBitmap)
thumbnailBitmap.recycle() // Clean up thumbnail bitmap after storing
}
closeQuietly(thumbnailReader)

cryptoFile.thumbnail = diskCache[cacheKey]
Timber.d("Successfully generated thumbnail for ${cryptoFile.name}")
} catch (e: OutOfMemoryError) {
closeQuietly(thumbnailReader)
Timber.e(e, "OutOfMemoryError during thumbnail generation for large image: ${cryptoFile.name} (${(cryptoFile.size ?: 0L) / (1024 * 1024)}MB)")
// Try to recover by forcing garbage collection
System.gc()
} catch (e: java.io.IOException) {
closeQuietly(thumbnailReader)
if (e.message?.contains("Pipe closed") == true) {
Timber.d("Thumbnail generation stream closed (expected for large files): ${cryptoFile.name}")
} else {
Timber.w(e, "IOException during thumbnail generation for file: ${cryptoFile.name}")
}
} catch (e: Exception) {
closeQuietly(thumbnailReader)
Timber.e(e, "Bitmap generation crashed for file: ${cryptoFile.name}")
}
}
}

protected fun generateCacheKey(cryptoFile: CryptoFile): String {
return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
}
Comment on lines +535 to +537
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use explicit locale in generateCacheKey method.

The generateCacheKey method effectively generates a unique key for caching thumbnails. However, using the default locale in String.format can lead to inconsistencies across different devices.

To ensure consistent behavior across all devices, specify an explicit locale:

 protected fun generateCacheKey(cryptoFile: CryptoFile): String {
-    return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
+    return String.format(Locale.US, "%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
 }

This change ensures that the cache key generation is consistent regardless of the device's locale settings.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected fun generateCacheKey(cryptoFile: CryptoFile): String {
return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
}
protected fun generateCacheKey(cryptoFile: CryptoFile): String {
return String.format(Locale.US, "%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
}
🧰 Tools
🪛 detekt

[warning] 482-482: String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode()) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)


private fun isThumbnailGenerationAvailable(cache: DiskLruCache?, fileName: String): Boolean {
return isGenerateThumbnailsEnabled() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.READONLY && cache != null && isImageMediaType(fileName)
}

fun associateThumbnails(list: List<CryptoNode>, progressAware: ProgressAware<FileTransferState>) {
if (!isGenerateThumbnailsEnabled()) {
return
}
val cryptoFileList = list.filterIsInstance<CryptoFile>()
if (cryptoFileList.isEmpty()) {
return
}
val firstCryptoFile = cryptoFileList[0]
val cloudType = (firstCryptoFile).cloudFile.cloud?.type() ?: return
val diskCache = getLruCacheFor(cloudType) ?: return
val toProcess = cryptoFileList.filter { cryptoFile ->
(isImageMediaType(cryptoFile.name) && cryptoFile.thumbnail == null)
}
var associated = 0
val elapsed = measureTimeMillis {
toProcess.forEach { cryptoFile ->
val cacheKey = generateCacheKey(cryptoFile)
val cacheFile = diskCache[cacheKey]
if (cacheFile != null && cryptoFile.thumbnail == null) {
cryptoFile.thumbnail = cacheFile
associated++
val state = FileTransferState { cryptoFile }
val progress = Progress.progress(state).thatIsCompleted()
progressAware.onProgress(progress)
}
}
}
Timber.tag("THUMBNAIL").i("[AssociateThumbnails] associated:${associated} files, elapsed:${elapsed}ms")
}

private fun isGenerateThumbnailsEnabled(): Boolean {
return sharedPreferencesHandler.useLruCache() && sharedPreferencesHandler.generateThumbnails() != ThumbnailsOption.NEVER
}

private fun storeThumbnail(cache: DiskLruCache?, cacheKey: String, thumbnailBitmap: Bitmap) {
val thumbnailFile: File = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache)
thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 100, thumbnailFile.outputStream())

try {
cache?.let {
LruFileCacheUtil.storeToLruCache(it, cacheKey, thumbnailFile)
} ?: Timber.tag("CryptoImplDecorator").e("Failed to store item in LRU cache")
} catch (e: IOException) {
Timber.tag("CryptoImplDecorator").e(e, "Failed to write the thumbnail in DiskLruCache")
}

thumbnailFile.delete()
}

private fun isImageMediaType(filename: String): Boolean {
return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image"
}

@Throws(BackendException::class, IOException::class)
private fun readToTmpFile(cryptoFile: CryptoFile, file: CloudFile, progressAware: ProgressAware<DownloadState>): File {
val encryptedTmpFile = File.createTempFile(UUID.randomUUID().toString(), ".crypto", internalCache)
Expand Down
Loading