Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@JustFanta01
Copy link
Contributor

@JustFanta01 JustFanta01 commented May 8, 2024

Hello guys 👋
This is our proposal for implementing thumbnail support [/issues/41]
Me, @WheelyMcBones and @taglioIsCoding have made the following changes.

We have implemented a uniform solution that works both for local and the remote clouds, this solution exploits the DiskLruCache in the CryptoImplDecorator and in the CryptoImplVaultFormat(Pre)7. We have decided these two location because we have access to all necessary informations: the decrypted image and the cloud type.

In cache we save a thumbnail when someone reads an image file and retrieve it from the same cache during the listing process. Thumbnails are stored as decrypted files in cache and, unlike other files in the /decrypted folder, these are persistent until the cache is deleted. We added the attribute ".thumbnail" in the CryptoFile pointing to the file in the disk cache and the CloudFileModel wraps it around.
We also added the Preference in the Settings for when it is supposed to generate the thumbnails.
Finally we got rid of the full duplication of the image by elaborating the thumbnail in stream with an ad-hoc Thread pool.

@coderabbitai
Copy link

coderabbitai bot commented May 8, 2024

Walkthrough

Adds configurable thumbnail generation UI and strings, implements per-cloud-type thumbnail LRU caching and on-demand thumbnail generation in the data layer, and integrates thumbnail association and download/processing workflows into file-browsing presenter and UI. UI changes include new preference entries, scroll-triggered thumbnail association for visible nodes, methods to replace images with a download icon, adapter support for showing thumbnail bitmaps, image-preview OOM handling, and a new thumbnail accessor on CloudFileModel. Manifest updated to request a larger heap.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Thumbnail support" directly and accurately summarizes the primary objective of this pull request. The changeset introduces comprehensive thumbnail generation and display functionality across multiple components, including presenter logic, UI fragment/activity updates, adapter modifications, and core thumbnail generation in the CryptoImplDecorator. A teammate reviewing the git history would immediately understand that this PR implements thumbnail support for both local and remote clouds. The title is concise and specific without being overly technical or containing noise.
Description Check ✅ Passed The pull request description clearly relates to the changeset by explaining the implementation approach for thumbnail support. It identifies the team members involved, describes the technical strategy of using DiskLruCache in specific locations to access decrypted image data and cloud type information, explains how thumbnails are cached persistently, documents the new CryptoFile ".thumbnail" attribute and CloudFileModel wrapper, mentions the user preference in Settings, and notes the thread pool optimization to avoid full image duplication. The description provides meaningful context about the changes rather than being vague or generic.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@JustFanta01 JustFanta01 mentioned this pull request May 8, 2024
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Review Details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits Files that changed from the base of the PR and between 45f7d0e and a487f6d.
Files ignored due to path filters (3)
  • presentation/src/main/res/values/arrays.xml is excluded by !**/*.xml
  • presentation/src/main/res/values/strings.xml is excluded by !**/*.xml
  • presentation/src/main/res/xml/preferences.xml is excluded by !**/*.xml
Files selected for processing (14)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt (2 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt (5 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt (3 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt (2 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (2 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (4 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt (2 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (2 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (6 hunks)
  • util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (2 hunks)
  • util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt (1 hunks)
  • util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt (2 hunks)
Files skipped from review due to trivial changes (2)
  • presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt
  • util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt
Additional comments not posted (22)
presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt (1)

14-14: Addition of thumbnail property to handle optional thumbnail files for CloudFileModel looks good.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt (1)

16-16: Addition of nullable thumbnail property in CryptoFile class is implemented correctly.

presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt (1)

37-43: Handling of thumbnail display in FileSettingsBottomSheet using BitmapFactory and conditional UI updates is implemented correctly.

util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt (1)

34-34: Addition of LOCAL option to the Cache enum to support local caching of thumbnails is appropriate.

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (1)

29-29: Import of SharedPreferencesHandler in BrowseFilesFragment for accessing preferences is correctly added.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt (1)

131-142: Integration of thumbnail caching in CryptoImplVaultFormatPre7 is implemented correctly, handling cache retrieval and setting thumbnails appropriately.

util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (1)

164-171: Implementation of generateThumbnails() in SharedPreferencesHandler to retrieve user preferences for thumbnail generation is correctly done.

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (3)

31-31: Ensure that the newly added constant THUMBNAIL_GENERATION is used consistently throughout the code.


259-259: Ensure the functionality of thumbnailGenerationChangeListener is implemented as it is crucial for handling user preferences changes.


342-342: The constant THUMBNAIL_GENERATION is appropriately declared.

presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (2)

151-157: Thumbnail display logic correctly checks if the file is an image and has a thumbnail before attempting to display it. Good use of Kotlin's safe call and smart cast.


159-160: The method isImageMediaType effectively determines if the file is an image based on its MIME type. This is crucial for the feature's correctness.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (5)

78-81: The thread pool for thumbnail generation is correctly configured with a custom thread factory. This is good for debugging and resource management.


83-85: Method getLruCacheFor correctly abstracts the retrieval of the LRU cache based on the cloud type. This encapsulation aids maintainability.


98-109: The renameFileInCache method correctly handles renaming of files in the cache. It checks if the old cache key exists before attempting to rename, which is a safe approach.


Line range hint 384-500: The method read handles the reading of files and the conditional generation of thumbnails. It uses a PipedOutputStream and PipedInputStream for thumbnail generation, which is an appropriate use of Java I/O for this purpose. However, ensure that resources are always closed in case of exceptions to avoid resource leaks.


446-466: The startThumbnailGeneratorThread method correctly handles the generation of thumbnails in a separate thread. It uses a future to manage the asynchronous task, which is a good practice for concurrent operations.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt (3)

168-179: Thumbnail caching logic added to the listing process.

This change integrates thumbnail caching into the file listing process, which should enhance the user experience by providing quick access to image previews. Ensure that the thumbnail generation and caching logic is thoroughly tested, especially in scenarios where the cache might become full or corrupted.


394-394: Ensure proper handling of file renaming in cache during file moves.

This method is crucial for maintaining consistency in the cache when files are moved. It's important to verify that it handles edge cases such as renaming non-existent files, concurrent access scenarios, and potential race conditions.


464-472: Properly handle thumbnail cache deletion during file deletion.

The implementation correctly checks for the existence of a thumbnail in the cache and deletes it if present. This is a necessary step to prevent orphaned cache entries which can lead to wasted storage space and potential privacy issues if sensitive thumbnails are not properly deleted.

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (2)

5-5: Ensure that BitmapFactory is used appropriately in the file to handle image processing for thumbnails.


517-517: The implementation for handling image previews appears to be correctly integrated with the new thumbnail feature. Ensure that the ImagePreviewFilesStore is properly tested, especially its interaction with the UI.

Comment on lines 150 to 152
private fun setupThumbnailGeneration() {
val preference = findPreference(THUMBNAIL_GENERATION) as Preference?
// TODO ...
}
Copy link

Choose a reason for hiding this comment

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

Complete the implementation of setupThumbnailGeneration or provide additional details on the intended functionality.

Comment on lines 114 to 116
private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
// TODO ...
true
}
Copy link

Choose a reason for hiding this comment

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

Complete the implementation of thumbnailGenerationChangeListener or provide additional details on the intended functionality.

@@ -1,5 +1,6 @@
package org.cryptomator.presentation.ui.adapter

import android.graphics.BitmapFactory
Copy link

Choose a reason for hiding this comment

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

Tip

Codebase Verification

The analysis of the BitmapFactory usage in CryptoImplDecorator.kt shows that it is configured safely. The BitmapFactory.Options is used with inSampleSize set to 4, effectively reducing the memory usage during image decoding. Additionally, the image is resized to a thumbnail, further mitigating memory concerns.

However, the other two files, FileSettingsBottomSheet.kt and BrowseFilesAdapter.kt, do not use any BitmapFactory.Options, which could potentially lead to unsafe configurations, especially in scenarios involving large images.

Recommended Actions:

  • Review and potentially refactor the usage of BitmapFactory.decodeFile in FileSettingsBottomSheet.kt and BrowseFilesAdapter.kt to include BitmapFactory.Options with appropriate settings to manage memory usage safely.
Analysis chain

Ensure BitmapFactory is used securely to avoid potential vulnerabilities related to image decoding.


The script results indicate that BitmapFactory.decodeFile and BitmapFactory.decodeStream are used in three different files. However, only in CryptoImplDecorator.kt is there an indication that BitmapFactory.Options might be used, which could potentially configure the decoding process safely. The other two usages in FileSettingsBottomSheet.kt and BrowseFilesAdapter.kt do not use any options, which might lead to unsafe configurations.

To confirm the safety of the usage in CryptoImplDecorator.kt and to provide specific guidance on what changes might be necessary in the other files, I need to inspect the options used in CryptoImplDecorator.kt.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify that BitmapFactory is used with safe configurations throughout the project.

# Test: Search for unsafe usage of BitmapFactory. Expect: No occurrences of unsafe configurations.
rg --type kotlin "BitmapFactory.decode"

Length of output: 512



Script:

#!/bin/bash
# Description: Inspect the BitmapFactory.Options used in CryptoImplDecorator.kt to determine if they configure safe usage.

# Test: Extract the relevant lines around the BitmapFactory.decodeStream usage to see the configuration of BitmapFactory.Options.
rg --context 5 "BitmapFactory.decodeStream" data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt

Length of output: 543

@infeo
Copy link
Member

infeo commented May 8, 2024

@JustFanta01 Not a review, but a general suggestion: For caching you use https://github.com/JakeWharton/DiskLruCache, which is unmaintained since 8 yrs. Consider using https://github.com/ben-manes/caffeine instead, which is a modern and quite good caching library.

Edit: my bad, this was the wrong dependency.

@SailReal
Copy link
Member

Thank you so much for this contribution ❤️, will have a closer look to it on Monday!

Consider using https://github.com/ben-manes/caffeine instead, which is a modern and quite good caching library.

@infeo can you please explain in detail why we should switch from DiskLruCache to Caffeine?

@infeo
Copy link
Member

infeo commented May 13, 2024

@SailReal I withdraw my suggestion^^ First, i thought this was an outdated, unmaintained dependency, but i was wrong. Second, the project already uses this dependency and then it is good practice to use what's already there. And third, the dependency targets Android, so i guess it is also "optimized" for the OS in some way.

Regarding Caffeine: It uses a different algorithm with a statistically higher hit rate. See also https://github.com/ben-manes/caffeine/wiki/Efficiency.

Copy link
Member

@SailReal SailReal left a comment

Choose a reason for hiding this comment

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

In general, it looks really good, thanks for your contribution!

Besides the comments in the code, here are some general thoughts:

  • Remove "Per folder" everywhere in the code, as it is not implemented yet.
  • Remove the Thumbnail category in the settings and move the setting to General to avoid having a category with only one entry.
  • I'll discuss this with Tobi, but I think the default should be to generate thumbnails by file, not never. Will come back with the result.
  • Always use brackets in if statements
  • The code is formatted almost everywhere, but some files are not completely 😅
  • Verify the performance impact of listing huge folders with a lot of generated thumbnails

Comment on lines 87 to 88
private fun getOrCreateLruCache(key: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? {
return diskLruCache.computeIfAbsent(key) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private fun getOrCreateLruCache(key: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? {
return diskLruCache.computeIfAbsent(key) {
private fun getOrCreateLruCache(cache: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? {
return diskLruCache.computeIfAbsent(cache) {

Comment on lines 89 to 93
val where = LruFileCacheUtil(context).resolve(it)
try {
DiskLruCache.create(where, cacheSize.toLong())
} catch (e: IOException) {
Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
val where = LruFileCacheUtil(context).resolve(it)
try {
DiskLruCache.create(where, cacheSize.toLong())
} catch (e: IOException) {
Timber.tag("CryptoImplDecorator").e(e, "Failed to setup LRU cache for $where.name")
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")

}
}
}
protected fun renameFileInCache(source: CryptoFile, target: CryptoFile){
Copy link
Member

Choose a reason for hiding this comment

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

Add a newline before this method and a blank before the {

Comment on lines 470 to 498
return buildString {
if (cloudFile.cloud?.id() != null)
this.append(cloudFile.cloud!!.id())
else
this.append("c") // "common"
this.append("-")
this.append(cloudFile.path.hashCode())
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return buildString {
if (cloudFile.cloud?.id() != null)
this.append(cloudFile.cloud!!.id())
else
this.append("c") // "common"
this.append("-")
this.append(cloudFile.path.hashCode())
}
return String.format("%s-%d", cloudFile.cloud?.id() ?: "common", cloudFile.path.hashCode())

}

private fun isGenerateThumbnailsEnabled(cache: DiskLruCache?, fileName: String): Boolean {
return sharedPreferencesHandler.useLruCache() &&
Copy link
Member

Choose a reason for hiding this comment

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

From a user perspective, it is currently not obvious that you need to enable caching for thumbnail generation to work. We may need to show a dialogue when changing the thumbnail generation from NONE to something else, and also for when caching gets disabled.

Even better, we could also decouple it here completely from the response cache so that we do not depend on this response caching feature, right?

)
} else if (!lowerFileName.endsWith(".gif") && isImageMediaType(cloudFile.name)) {
val cloudFileNodes = previewCloudFileNodes

Copy link
Member

Choose a reason for hiding this comment

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

Remove newline

Comment on lines 41 to 42
if(iv_file_image.drawable == null)
iv_file_image.setImageResource(cloudFileModel.icon.iconResource)
Copy link
Member

Choose a reason for hiding this comment

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

Please use brackets and auto format.

navigationMode?.let { cloudNodesAdapter.updateNavigationMode(it) }

recyclerView.layoutManager = LinearLayoutManager(context())
// recyclerView.layoutManager = GridLayoutManager(context(), 2)
Copy link
Member

Choose a reason for hiding this comment

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

Remove unused layout

Comment on lines 150 to 152
private fun setupThumbnailGeneration() {
val preference = findPreference(THUMBNAIL_GENERATION) as Preference?
// TODO ...
}
Copy link
Member

Choose a reason for hiding this comment

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

Can be removed

Comment on lines 114 to 116
private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
// TODO ...
true
}
Copy link
Member

Choose a reason for hiding this comment

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

Can be removed

@SailReal
Copy link
Member

SailReal commented May 14, 2024

Also there is a bug if you use an svg-file, then BitmapFactory.decodeStream(thumbnailReader, null, options) returns null, thumbnailBitmap is then null (I think we shouldn't even call ThumbnailUtils.extractThumbnail if it is null) and then it crashes when we write to it in thumbnailWriter.write(buff.array(), 0, buff.remaining()) because we didn't even create the file we want to write into

org.cryptomator.domain.exception.FatalBackendException: java.io.IOException: Pipe closed
    	at org.cryptomator.data.cloud.crypto.CryptoImplDecorator.read(CryptoImplDecorator.kt:422)
    	at org.cryptomator.data.cloud.crypto.CryptoCloudContentRepository.read(CryptoCloudContentRepository.kt:95)
    	at org.cryptomator.data.cloud.crypto.CryptoCloudContentRepository.read(CryptoCloudContentRepository.kt:21)
    	at org.cryptomator.data.repository.DispatchingCloudContentRepository.read(DispatchingCloudContentRepository.kt:160)
    	at org.cryptomator.domain.usecases.cloud.DownloadFiles.execute(DownloadFiles.java:32)
    	at org.cryptomator.domain.usecases.cloud.DownloadFilesUseCase$Launcher$2.subscribe(DownloadFilesUseCase.java:99)
    	at io.reactivex.internal.operators.flowable.FlowableFromPublisher.subscribeActual(FlowableFromPublisher.java:29)
    	at io.reactivex.Flowable.subscribe(Flowable.java:14935)
    	at io.reactivex.Flowable.subscribe(Flowable.java:14882)
    	at io.reactivex.internal.operators.flowable.FlowableSubscribeOn$SubscribeOnSubscriber.run(FlowableSubscribeOn.java:82)
    	at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker$BooleanRunnable.run(ExecutorScheduler.java:288)
    	at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker.run(ExecutorScheduler.java:253)
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    	at java.lang.Thread.run(Thread.java:920)
    Caused by: java.io.IOException: Pipe closed
    	at java.io.PipedInputStream.checkStateForReceive(PipedInputStream.java:263)
    	at java.io.PipedInputStream.awaitSpace(PipedInputStream.java:271)
    	at java.io.PipedInputStream.receive(PipedInputStream.java:234)
    	at java.io.PipedOutputStream.write(PipedOutputStream.java:149)
    	at org.cryptomator.data.cloud.crypto.CryptoImplDecorator.read(CryptoImplDecorator.kt:398)
    	at org.cryptomator.data.cloud.crypto.CryptoCloudContentRepository.read(CryptoCloudContentRepository.kt:95) 
    	at org.cryptomator.data.cloud.crypto.CryptoCloudContentRepository.read(CryptoCloudContentRepository.kt:21) 
    	at org.cryptomator.data.repository.DispatchingCloudContentRepository.read(DispatchingCloudContentRepository.kt:160) 
    	at org.cryptomator.domain.usecases.cloud.DownloadFiles.execute(DownloadFiles.java:32) 
    	at org.cryptomator.domain.usecases.cloud.DownloadFilesUseCase$Launcher$2.subscribe(DownloadFilesUseCase.java:99) 
    	at io.reactivex.internal.operators.flowable.FlowableFromPublisher.subscribeActual(FlowableFromPublisher.java:29) 
    	at io.reactivex.Flowable.subscribe(Flowable.java:14935) 
    	at io.reactivex.Flowable.subscribe(Flowable.java:14882) 
    	at io.reactivex.internal.operators.flowable.FlowableSubscribeOn$SubscribeOnSubscriber.run(FlowableSubscribeOn.java:82) 
    	at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker$BooleanRunnable.run(ExecutorScheduler.java:288) 
    	at io.reactivex.internal.schedulers.ExecutorScheduler$ExecutorWorker.run(ExecutorScheduler.java:253) 
    	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
    	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
    	at java.lang.Thread.run(Thread.java:920) 
    
ErrorCode: F1D9:OBF0

In this case, the file can not be opened anymore.

Please also test it with further other file types.

@JustFanta01 JustFanta01 force-pushed the feature/thumbnail-playground branch from a487f6d to e05f206 Compare September 9, 2024 16:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Outside diff range and nitpick comments (20)
util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt (1)

3-8: Add KDoc comments to improve code documentation.

While the enum is straightforward, adding KDoc comments would enhance its usability and maintainability. Consider adding a class-level comment explaining the purpose of this enum and brief comments for each constant to clarify their meanings.

Here's a suggested implementation with KDoc comments:

/**
 * Represents the various options for thumbnail generation and management in the application.
 */
enum class ThumbnailsOption {
    /** Never generate thumbnails. */
    NEVER,
    
    /** Generate thumbnails for read-only operations. */
    READONLY,
    
    /** Generate thumbnails on a per-file basis. */
    PER_FILE,
    
    /** Generate thumbnails on a per-folder basis. */
    PER_FOLDER
}
domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java (3)

18-22: LGTM: Constructor is well-implemented with a minor suggestion.

The constructor properly initializes the fields and follows the principle of dependency injection. The use of the @Parameter annotation on the list parameter is noted.

Consider adding a null check for the cloudContentRepository parameter to ensure it's not null, as it's a critical dependency:

 public AssociateThumbnails(CloudContentRepository cloudContentRepository, //
 		@Parameter List<CloudNode> list) {
+	if (cloudContentRepository == null) {
+		throw new IllegalArgumentException("cloudContentRepository must not be null");
+	}
 	this.cloudContentRepository = cloudContentRepository;
 	this.list = list;
 }

24-26: LGTM: Execute method is well-implemented with a minor suggestion.

The execute method is appropriately defined and follows good practices:

  • It takes a ProgressAware<FileTransferState> parameter for progress tracking.
  • It can throw a BackendException for error handling.
  • It correctly delegates the work to the repository, adhering to the separation of concerns principle.

Consider adding a null check for the progressAware parameter to provide a clearer error message if it's null:

 public void execute(ProgressAware<FileTransferState> progressAware) throws BackendException {
+	if (progressAware == null) {
+		throw new IllegalArgumentException("progressAware must not be null");
+	}
 	cloudContentRepository.associateThumbnails(list, progressAware);
 }

1-27: Overall assessment: Well-implemented use case for thumbnail association.

This new AssociateThumbnails class is a well-structured implementation of the thumbnail association use case. It aligns with the PR objectives and follows good software engineering practices:

  1. Clear separation of concerns
  2. Dependency injection
  3. Proper error handling
  4. Use of custom annotations for better code organization

The suggested minor improvements (null checks) would further enhance the robustness of the code.

Consider adding unit tests for this class to ensure its behavior is correct, especially testing edge cases and error scenarios.

presentation/src/main/res/values/arrays.xml (2)

45-50: LGTM! Consider clarifying the "per folder" option.

The thumbnail_generation_entries array is well-structured and provides a good range of options for thumbnail generation. It follows the existing pattern in the file and uses string resources, which is great for localization.

Consider adding a comment or documentation to clarify that the "per folder" option (@string/thumbnail_generation_folder) is not yet implemented, as mentioned in the PR objectives. This will help prevent confusion for other developers or users.


45-57: Overall, good addition of thumbnail generation options.

The new thumbnail_generation_entries and thumbnail_generation_values arrays are well-structured and provide valuable configuration options for thumbnail generation in the application. They align with the PR objectives and follow the existing patterns in the file.

A few points to consider:

  1. Clarify the status of the "per folder" option in comments or documentation.
  2. Ensure consistency between the arrays and the actual implementation, especially regarding the "PER_FOLDER" option.
  3. These changes will likely require corresponding updates in the application's settings UI and backend logic to handle the new options.

As you implement the logic for these new thumbnail generation options, consider the following:

  • Ensure that the option selected by the user is properly persisted and retrieved.
  • Implement appropriate logic in the thumbnail generation process to respect these settings.
  • Update the UI to reflect the chosen option and possibly disable the "PER_FOLDER" option if it's not yet implemented.
  • Consider adding unit tests to verify that the thumbnail generation behavior correctly follows the selected option.
domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt (2)

98-101: LGTM: New method associateThumbnails added

The new method associateThumbnails is well-structured and consistent with the interface's style. It aligns with the PR objectives of implementing thumbnail functionality.

Consider adding KDoc comments to describe the method's purpose, parameters, and potential exceptions. For example:

/**
 * Associates thumbnails with a list of cloud nodes.
 *
 * @param list The list of cloud nodes to associate thumbnails with.
 * @param progressAware An object to track the progress of the thumbnail association process.
 * @throws BackendException if there's an error during the thumbnail association process.
 */
@Throws(BackendException::class)
fun associateThumbnails(list: List<NodeType>, progressAware: ProgressAware<FileTransferState>) {
    // default implementation
}

Line range hint 1-102: Summary: Thumbnail support added to CloudContentRepository interface

The changes to CloudContentRepository.kt are minimal and focused, adding support for thumbnail functionality as outlined in the PR objectives. The new associateThumbnails method provides a clear extension point for implementing thumbnail association in concrete classes. The interface remains flexible and consistent with its existing design.

As the thumbnail functionality is implemented, consider the following architectural points:

  1. Ensure that concrete implementations of this interface handle potential errors gracefully, especially when dealing with different file types (e.g., SVG files as mentioned in the PR comments).
  2. Consider adding a method to retrieve thumbnails, which might be useful for UI components that display file listings with thumbnails.
  3. As the thumbnail feature evolves, you might want to add methods for managing the thumbnail cache (e.g., clearing, updating, or checking cache status) in this interface or a related one.
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt (2)

99-102: LGTM: New method associateThumbnails implemented correctly.

The new method is well-implemented and aligns with the PR objectives for thumbnail functionality. It correctly delegates the call to the cryptoImpl instance and uses ProgressAware for tracking progress.

Consider adding a KDoc comment to describe the purpose of this method and its parameters. For example:

/**
 * Associates thumbnails with a list of CryptoNode objects.
 *
 * @param list The list of CryptoNode objects to associate thumbnails with.
 * @param progressAware A ProgressAware object to track the progress of the operation.
 * @throws BackendException if there's an error during the thumbnail association process.
 */
@Throws(BackendException::class)
override fun associateThumbnails(list: List<CryptoNode>, progressAware: ProgressAware<FileTransferState>) {
    cryptoImpl.associateThumbnails(list, progressAware)
}

This documentation will improve code readability and maintainability.


Line range hint 1-140: Summary: Thumbnail functionality successfully implemented.

The changes in this file effectively implement the thumbnail functionality as described in the PR objectives. The new associateThumbnails method is well-integrated into the existing CryptoCloudContentRepository class, following the established patterns and coding style.

Key points:

  1. The new import and method are correctly placed and implemented.
  2. The changes are minimal and focused, reducing the risk of unintended side effects.
  3. The use of ProgressAware allows for tracking the progress of thumbnail association, which is beneficial for user experience.

These changes contribute to the overall goal of providing thumbnail support for both local and remote cloud storage in the Cryptomator Android application.

As the thumbnail functionality evolves, consider the following architectural points:

  1. Ensure that the cryptoImpl implementations (e.g., CryptoImplVaultFormat8, CryptoImplVaultFormat7, etc.) have consistent implementations of the associateThumbnails method.
  2. Monitor the performance impact of thumbnail generation and association, especially for large folders or slow network connections.
  3. Consider implementing caching strategies to optimize thumbnail retrieval in future iterations.
data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt (1)

168-180: Good implementation, but consider adding documentation and a safety check.

The associateThumbnails method is well-implemented and follows the existing patterns in the class. However, there are a couple of suggestions for improvement:

  1. Add KDoc documentation to explain the purpose of the method and its parameters.
  2. Consider adding a check to ensure all nodes in the list belong to the same cloud, as the current implementation assumes this is the case.

Here's a suggested implementation with these improvements:

/**
 * Associates thumbnails with the given list of CloudNodes.
 *
 * @param list The list of CloudNodes to associate thumbnails with.
 * @param progressAware A ProgressAware object to track the file transfer state.
 * @throws IllegalArgumentException if the list contains nodes from different clouds.
 * @throws IllegalStateException if the cloud is null for any node.
 * @throws AuthenticationException if there's an authentication error.
 */
@Throws(BackendException::class)
override fun associateThumbnails(list: List<CloudNode>, progressAware: ProgressAware<FileTransferState>) {
    if (list.isEmpty()) {
        return
    }
    
    val cloud = list[0].cloud ?: throw IllegalStateException("Cloud shouldn't be null")
    
    // Ensure all nodes belong to the same cloud
    if (list.any { it.cloud != cloud }) {
        throw IllegalArgumentException("All nodes must belong to the same cloud")
    }
    
    try {
        networkConnectionCheck.assertConnectionIsPresent(cloud)
        delegateFor(list[0]).associateThumbnails(list, progressAware)
    } catch (e: AuthenticationException) {
        delegates.remove(cloud)
        throw e
    }
}

This implementation adds documentation, ensures all nodes belong to the same cloud, and simplifies the null check for the cloud.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt (3)

131-131: Approve with suggestion: Consider logging unexpected null values.

The addition of .filterNotNull() is a good defensive programming practice that aligns with Kotlin's null safety features. It prevents potential null pointer exceptions in the code that consumes this list.

However, if null values are not expected in this list, their presence might indicate an underlying issue that should be addressed.

Consider adding logging for unexpected null values before filtering them out. This can help in identifying and addressing the root cause of null values. For example:

.map { node ->
    ciphertextToCleartextNode(cryptoFolder, dirId, node)
}.toList().onEach { if (it == null) Timber.w("Unexpected null value in list for folder: ${cryptoFolder.name}") }.filterNotNull()

229-229: Approve with suggestion: Add error handling for cache renaming.

The addition of renameFileInCache(source, target) is a good step to maintain consistency between the file system and the cache during move operations. This aligns with the thumbnail caching functionality mentioned in the PR objectives.

However, it's important to ensure that the move operation remains robust even if the cache renaming fails.

Consider adding error handling for the cache renaming operation. This will ensure that the move operation completes successfully even if there's an issue with updating the cache. For example:

try {
    renameFileInCache(source, target)
} catch (e: Exception) {
    Timber.e(e, "Failed to rename file in cache during move operation. Source: ${source.name}, Target: ${target.name}")
    // Optionally, you might want to invalidate the cache entry if renaming fails
    // invalidateCacheEntry(source)
}

250-258: Approve with suggestions: Add error handling and consider potential race conditions.

The addition of cache deletion logic when deleting a CryptoFile is a good improvement. It ensures that the cache remains consistent with the actual file system, preventing potential issues with stale cached data.

However, there are a few points to consider:

  1. Error Handling: Add try-catch block to handle potential exceptions during cache deletion.
  2. Race Conditions: Consider potential race conditions where the cache entry might be accessed while being deleted.
  3. Logging: Add logging for cache deletion operations for better traceability.

Here's a suggested implementation addressing these points:

val cacheKey = generateCacheKey(node)
node.cloudFile.cloud?.type()?.let { cloudType ->
    getLruCacheFor(cloudType)?.let { diskCache ->
        try {
            diskCache.edit(cacheKey)?.let { editor ->
                editor.abort()  // Prevent further access to this cache entry
                diskCache.remove(cacheKey)
                Timber.d("Successfully deleted cache entry for file: ${node.name}")
            }
        } catch (e: Exception) {
            Timber.e(e, "Failed to delete cache entry for file: ${node.name}")
        }
    }
}

This implementation uses the edit() and abort() methods to prevent race conditions, adds error handling, and includes logging for better traceability.

util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (2)

164-172: LGTM! Consider using an enum for better type safety.

The generateThumbnails() method is well-implemented and correctly handles different thumbnail generation options. It provides a good default value and uses Kotlin's when expression effectively.

To improve type safety and readability, consider defining an enum for the thumbnail generation options:

enum class ThumbnailGenerationOption(val value: String) {
    NEVER("NEVER"),
    READONLY("READONLY"),
    PER_FILE("PER_FILE"),
    PER_FOLDER("PER_FOLDER");

    companion object {
        fun fromString(value: String): ThumbnailGenerationOption = 
            values().find { it.value == value } ?: NEVER
    }
}

Then, update the generateThumbnails() method:

fun generateThumbnails(): ThumbnailsOption {
    val option = defaultSharedPreferences.getValue(THUMBNAIL_GENERATION, ThumbnailGenerationOption.NEVER.value)
    return ThumbnailGenerationOption.fromString(option).toThumbnailsOption()
}

private fun ThumbnailGenerationOption.toThumbnailsOption(): ThumbnailsOption {
    return when (this) {
        ThumbnailGenerationOption.NEVER -> ThumbnailsOption.NEVER
        ThumbnailGenerationOption.READONLY -> ThumbnailsOption.READONLY
        ThumbnailGenerationOption.PER_FILE -> ThumbnailsOption.PER_FILE
        ThumbnailGenerationOption.PER_FOLDER -> ThumbnailsOption.PER_FOLDER
    }
}

This approach provides better type safety and makes it easier to add or modify options in the future.


164-172: Thumbnail-related changes are well-integrated.

The new generateThumbnails() method and THUMBNAIL_GENERATION constant are seamlessly integrated into the SharedPreferencesHandler class. They follow the existing patterns and conventions, maintaining consistency with the rest of the codebase.

To further improve the implementation, consider adding a method to set the thumbnail generation option:

fun setThumbnailGeneration(option: ThumbnailsOption) {
    defaultSharedPreferences.setValue(THUMBNAIL_GENERATION, option.name)
}

This would provide a convenient way to update the thumbnail generation setting and ensure type safety when setting the value.

Also applies to: 331-331

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (2)

341-341: Consider using a resource string for the thumbnail generation preference key

The addition of the THUMBNAIL_GENERATION constant is good for managing the thumbnail generation preference. However, to improve consistency and maintainability, consider using a resource string for the preference key instead of a hardcoded string.

Here's a suggested improvement:

  1. Add a new string resource in your strings.xml file:
<string name="pref_key_thumbnail_generation" translatable="false">thumbnailGeneration</string>
  1. Update the constant in the SettingsFragment:
private const val THUMBNAIL_GENERATION = R.string.pref_key_thumbnail_generation
  1. Update the usage in useLruChangedListener:
findPreference<ListPreference>(getString(THUMBNAIL_GENERATION))?.let { preference ->
    // ... existing code ...
}

This approach ensures consistency across the app and makes it easier to update the key if needed in the future.


Line range hint 1-341: Summary of SettingsFragment changes

The changes to the SettingsFragment implement the thumbnail generation preference as described in the PR objectives. The new functionality is logically tied to the LRU cache setting, which is appropriate. However, there are several areas where the implementation can be improved:

  1. The setupThumbnailGeneration method is still missing and should be implemented to properly initialize the thumbnail generation preference.
  2. The handling of the LRU cache and thumbnail generation settings in useLruChangedListener can be refactored for better organization and error handling.
  3. Consider using a resource string for the THUMBNAIL_GENERATION constant to improve maintainability and consistency.

These improvements will enhance the overall quality and robustness of the implementation. Please address these points in your next iteration.

To further improve the code:

  1. Consider extracting the thumbnail generation logic into a separate class or helper methods to keep the SettingsFragment focused on UI-related tasks.
  2. Implement proper error handling for cases where preferences might not be found.
  3. Ensure that the state of the thumbnail generation preference is properly persisted and restored across app restarts.
  4. Add unit tests for the new functionality to ensure its correctness and prevent regressions.
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt (1)

452-460: Good addition of cache cleanup during file deletion.

The new code ensures that the cache is updated when a file is deleted, maintaining consistency between the cache and the file system. This is a valuable improvement to prevent stale cache entries.

Consider adding error handling for potential exceptions during cache operations to improve robustness.

Consider wrapping the cache operations in a try-catch block to handle potential exceptions:

 val cacheKey = generateCacheKey(node)
 node.cloudFile.cloud?.type()?.let { cloudType ->
   getLruCacheFor(cloudType)?.let { diskCache ->
-    if (diskCache[cacheKey] != null) {
-      diskCache.delete(cacheKey)
+    try {
+      if (diskCache[cacheKey] != null) {
+        diskCache.delete(cacheKey)
+      }
+    } catch (e: Exception) {
+      Timber.tag("CryptoFs").e(e, "Failed to delete cache entry for %s", cacheKey)
     }
   }
 }
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (1)

89-89: Remove unnecessary @Override annotations in Kotlin

The @Override annotation is not used in Kotlin when overriding methods; instead, the override keyword is sufficient. You can remove these annotations to clean up the code.

Apply this diff:

 private val onFastScrollStateChangeListener = object : OnFastScrollStateChangeListener {
-    @Override
     override fun onFastScrollStop() {
         thumbnailsForVisibleNodes()
     }

-    @Override
     override fun onFastScrollStart() {
     }
 }

 private val onScrollListener = object : RecyclerView.OnScrollListener() {
-    @Override
     override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
         super.onScrollStateChanged(recyclerView, newState)
         if (newState == SCROLL_STATE_IDLE) {

Also applies to: 94-94, 100-100

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between e05f206 and d85096b.

📒 Files selected for processing (15)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt (2 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt (3 hunks)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt (3 hunks)
  • data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt (2 hunks)
  • domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt (2 hunks)
  • domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (7 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (4 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (3 hunks)
  • presentation/src/main/res/values/arrays.xml (1 hunks)
  • presentation/src/main/res/values/strings.xml (2 hunks)
  • presentation/src/main/res/xml/preferences.xml (1 hunks)
  • util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (2 hunks)
  • util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt (1 hunks)
🧰 Additional context used
🪛 detekt
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt

[warning] 124-124: A call to the default constructor of an exception was detected. Instead one of the constructor overloads should be called. This allows to provide more meaningful exceptions.

(detekt.exceptions.ThrowingExceptionsWithoutMessageOrCause)


[warning] 449-449: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


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

(detekt.potential-bugs.ImplicitDefaultLocale)

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt

[warning] 95-96: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

🔇 Additional comments (15)
util/src/main/java/org/cryptomator/util/ThumbnailsOption.kt (1)

1-8: Overall assessment: Good implementation with room for improvement.

The ThumbnailsOption enum is well-structured and aligns with the PR objectives for implementing thumbnail support. It provides a clear set of options for thumbnail generation and management. However, adding KDoc comments and addressing the unimplemented PER_FOLDER option would significantly enhance its clarity and usability.

To ensure this enum is being used correctly throughout the project, let's run a quick check:

This script will help verify the integration of the new enum in the project and identify any premature usage of the unimplemented PER_FOLDER option.

domain/src/main/java/org/cryptomator/domain/usecases/cloud/AssociateThumbnails.java (2)

1-11: LGTM: Package declaration and imports are appropriate.

The package declaration and imports are well-organized and relevant to the functionality of the class. The use of custom annotations (@Parameter and @UseCase) indicates a structured approach to use case implementation.


12-17: LGTM: Class declaration and fields are well-defined.

The class is properly annotated as a use case, and the fields are correctly declared as private and final. The types of the fields (CloudContentRepository and List) are appropriate for the intended functionality.

presentation/src/main/res/values/arrays.xml (1)

51-56: LGTM! Ensure consistency with implementation status.

The thumbnail_generation_values array is well-structured and provides clear, descriptive values that correspond to the entries in the thumbnail_generation_entries array.

To ensure consistency between the arrays and the actual implementation, please verify that the "PER_FOLDER" option is handled correctly in the code. If it's not yet implemented, consider one of the following options:

  1. Remove the "PER_FOLDER" option from both arrays until it's implemented.
  2. Keep it in the arrays but ensure it's properly handled in the code (e.g., showing as disabled or with a "coming soon" indicator).

You can use the following script to check for the usage of "PER_FOLDER" in the codebase:

This will help ensure that the UI options align with the actual implementation.

✅ Verification successful

Verification Successful: "PER_FOLDER" is correctly implemented.

The usage of "PER_FOLDER" has been confirmed in both Kotlin and XML files, ensuring consistency between the thumbnail_generation_values array and the actual implementation in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for usage of "PER_FOLDER" option in the codebase

# Search for "PER_FOLDER" in Kotlin files
echo "Searching for 'PER_FOLDER' usage in Kotlin files:"
rg --type kotlin "PER_FOLDER"

# Search for "thumbnail_generation_folder" in XML files
echo "Searching for 'thumbnail_generation_folder' usage in XML files:"
rg --type xml "thumbnail_generation_folder"

Length of output: 952

domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt (1)

11-11: LGTM: New import for FileTransferState

The import of FileTransferState is appropriate and necessary for the new associateThumbnails method.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt (1)

16-16: LGTM: Import statement added correctly.

The new import for FileTransferState is necessary for the newly added associateThumbnails method and follows the existing import style.

presentation/src/main/res/xml/preferences.xml (1)

144-151: LGTM! Consider adding a comment for clarity.

The new ListPreference for thumbnail generation is well-structured and appropriately placed within the "LRU cache" category. The attributes are correctly set, using string resources for localization and following Android best practices.

Consider adding a comment above this preference to explain its purpose and potential impact on the app's behavior. This would be helpful for future maintenance.

Also, ensure that the following resources are defined and appropriate:

  • @string/dialog_thumbnail_generation_title
  • @array/thumbnail_generation_entries
  • @array/thumbnail_generation_values

To verify the uniqueness of the preference key and the existence of required resources, you can run the following script:

✅ Verification successful

Preference Key and Resources Verified

The thumbnailGeneration preference key is unique within the codebase. All required string and array resources (dialog_thumbnail_generation_title, thumbnail_generation_entries, thumbnail_generation_values) are present and correctly defined.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify uniqueness of preference key and existence of required resources

# Check for duplicate keys
echo "Checking for duplicate preference keys:"
grep -n 'android:key="thumbnailGeneration"' $(find . -name '*.xml')

# Check for required string resource
echo "Checking for dialog_thumbnail_generation_title string resource:"
grep -n 'name="dialog_thumbnail_generation_title"' $(find . -name 'strings.xml')

# Check for required array resources
echo "Checking for thumbnail_generation_entries array resource:"
grep -n 'name="thumbnail_generation_entries"' $(find . -name 'arrays.xml')

echo "Checking for thumbnail_generation_values array resource:"
grep -n 'name="thumbnail_generation_values"' $(find . -name 'arrays.xml')

Length of output: 21729

data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt (1)

17-17: LGTM: Import statement added correctly.

The new import for FileTransferState is necessary for the new associateThumbnails method and follows the existing import style.

util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (2)

331-331: LGTM! Constant is well-defined.

The THUMBNAIL_GENERATION constant is correctly defined and follows the established naming convention in the companion object.


164-172: Overall assessment: Thumbnail generation changes are well-implemented.

The additions for thumbnail generation support in the SharedPreferencesHandler class are well-designed and properly integrated. The code follows existing patterns and maintains consistency with the rest of the class. The suggestions provided for using an enum and adding a setter method would further enhance type safety and usability.

Great job on implementing this feature! The changes align well with the PR objectives of adding thumbnail support to the Cryptomator Android application.

Also applies to: 331-331

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormat7.kt (3)

382-382: Good addition of cache management during file move operations.

The new call to renameFileInCache is a valuable improvement. It ensures that the cache is updated when a file is moved, maintaining consistency between the cache and the actual file system. This addition enhances the overall reliability of the file management system.


Line range hint 1-577: Overall assessment: Good improvements to cache management and data processing.

The changes in this file enhance the consistency between the file system and the cache, particularly during file move and delete operations. The addition of data decoration before writing is also a positive change. While no major issues were identified, consider implementing the suggested error handling improvement for cache operations to further increase the robustness of the system.


505-505: Appropriate use of data decoration before writing.

The addition of data.decorate() is a good practice, likely applying necessary transformations or additional processing to the data before writing. This aligns well with the cryptographic nature of the class.

To ensure this change doesn't introduce any unintended side effects, could you provide more information about the decorate method? Specifically:

  1. What operations does it perform on the data?
  2. Are there any performance implications?
  3. How does it interact with the encryption process?
✅ Verification successful

Verified: Addition of data.decorate() aligns with existing patterns.

The use of data.decorate() in the specified location is consistent with its implementation across other components, ensuring proper data transformation before writing.

  • decorate method is defined and utilized appropriately in related classes:
    • DataSource.kt
    • UriBasedDataSource.kt
    • CryptoImplDecorator.kt
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for the definition and usages of the 'decorate' method
echo "Searching for 'decorate' method definition:"
ast-grep --lang kotlin --pattern 'fun decorate($$$): $$$'

echo "\nSearching for 'decorate' method usages:"
rg '\bdecorate\b' --type kotlin

Length of output: 1525

presentation/src/main/res/values/strings.xml (1)

635-640: New string resources for thumbnail generation look good.

The new string resources for thumbnail generation settings have been added correctly. They follow the existing naming conventions and are properly formatted. Here's a breakdown of the changes:

  1. Four options for thumbnail generation have been added: "Never", "Read Only", "Generate Per File", and "Generate Per Folder".
  2. A toast message for when LRU cache is disabled has been included.
  3. A title for the thumbnail generation dialog has been added.

These additions enhance the app's capability to manage thumbnail generation preferences, providing users with more control over how thumbnails are created and displayed.

Also applies to: 650-650

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (1)

130-131: Ensure RecyclerView supports fast scroll listener methods

Verify that setOnFastScrollStateChangeListener is available for your RecyclerView. If you're using a custom RecyclerView or a library that adds this method, ensure it is properly integrated. If not, you may need to implement fast scroll functionality differently.

Run the following script to check for the method's availability:

NEVER,
READONLY,
PER_FILE,
PER_FOLDER
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Address the unimplemented PER_FOLDER option.

The PR objectives mention that the PER_FOLDER option is currently not implemented. It's important to clearly indicate this in the code to prevent misuse and provide clarity for other developers.

Consider one of the following approaches:

  1. Add a TODO comment to remind developers about the unimplemented state.
  2. Use the @Deprecated annotation with a message explaining that it's not yet implemented.
  3. If it's not intended for use at all, consider removing it from the enum.

Here's an example using the @Deprecated annotation:

/** Generate thumbnails on a per-folder basis. */
@Deprecated("Not yet implemented")
PER_FOLDER

Comment on lines +84 to +94

findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = false
}
Toast.makeText(context, context?.getString(R.string.thumbnail_generation__deactivation_toast), Toast.LENGTH_LONG).show()
}

if (TRUE == newValue) {
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = true
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve handling of LRU cache and thumbnail generation settings

The changes implement a logical dependency between the LRU cache and thumbnail generation settings. However, there are a few improvements that can be made:

  1. The toast message for LRU cache changes is displayed regardless of whether the value actually changed. Consider only showing the toast when the value changes.
  2. When the LRU cache is enabled, consider calling a method to restore the previous state of the thumbnail generation preference, as it might have been enabled before the LRU cache was disabled.
  3. The implementation of setupThumbnailGeneration is still missing, as mentioned in a past review comment. This method should be implemented to properly initialize the thumbnail generation preference.

Here's a suggested improvement:

private val useLruChangedListener = Preference.OnPreferenceChangeListener { _, newValue ->
    val lruEnabled = newValue as Boolean
    if (lruEnabled != sharedPreferencesHandler.useLruCache()) {
        if (!lruEnabled) {
            LruFileCacheUtil(requireContext()).clear()
            setupLruCacheSize()
            disableThumbnailGeneration()
        } else {
            restoreThumbnailGenerationState()
        }
        Toast.makeText(context, context?.getString(R.string.screen_settings_lru_cache_changed__restart_toast), Toast.LENGTH_SHORT).show()
    }
    true
}

private fun disableThumbnailGeneration() {
    findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
        preference.isSelectable = false
    }
    Toast.makeText(context, context?.getString(R.string.thumbnail_generation__deactivation_toast), Toast.LENGTH_LONG).show()
}

private fun restoreThumbnailGenerationState() {
    findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
        preference.isSelectable = true
        // Restore the previous state if needed
    }
}

// TODO: Implement this method
private fun setupThumbnailGeneration() {
    // Initialize the thumbnail generation preference
}

Don't forget to call setupThumbnailGeneration() in the onCreatePreferences method.

import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE
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 RecyclerView.SCROLL_STATE_IDLE instead of AbsListView

The constant SCROLL_STATE_IDLE is being imported from android.widget.AbsListView.OnScrollListener, which is intended for ListView. Since you are working with RecyclerView, you should import SCROLL_STATE_IDLE from androidx.recyclerview.widget.RecyclerView to ensure consistency and avoid potential issues.

Apply this diff to fix the import:

-import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE
+import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
📝 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
import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE

Comment on lines +95 to +96
override fun onFastScrollStart() {
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove empty onFastScrollStart method

The onFastScrollStart method is empty and can be removed if no action is required when fast scrolling starts. This will simplify your code.

Apply this diff to remove the empty method:

     override fun onFastScrollStop() {
         thumbnailsForVisibleNodes()
     }

-    override fun onFastScrollStart() {
-    }
 }
📝 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
override fun onFastScrollStart() {
}
override fun onFastScrollStop() {
thumbnailsForVisibleNodes()
}
}
🧰 Tools
🪛 detekt

[warning] 95-96: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

Comment on lines 145 to 147
val first = layoutManager.findFirstVisibleItemPosition()
val last = layoutManager.findLastVisibleItemPosition()
val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle potential -1 values from findFirstVisibleItemPosition() and findLastVisibleItemPosition()

The methods findFirstVisibleItemPosition() and findLastVisibleItemPosition() can return -1 if no items are visible, which could lead to an IndexOutOfBoundsException when calling subList. Consider adding a check to ensure that first and last are valid indices before proceeding.

Apply this diff to handle the potential issue:

 val layoutManager = binding.recyclerViewLayout.recyclerView.layoutManager as LinearLayoutManager
 val first = layoutManager.findFirstVisibleItemPosition()
 val last = layoutManager.findLastVisibleItemPosition()
+if (first != RecyclerView.NO_POSITION && last != RecyclerView.NO_POSITION) {
     val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
     if (!binding.swipeRefreshLayout.isRefreshing) {
         browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes)
     }
+}
📝 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
val first = layoutManager.findFirstVisibleItemPosition()
val last = layoutManager.findLastVisibleItemPosition()
val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
val first = layoutManager.findFirstVisibleItemPosition()
val last = layoutManager.findLastVisibleItemPosition()
if (first != RecyclerView.NO_POSITION && last != RecyclerView.NO_POSITION) {
val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
if (!binding.swipeRefreshLayout.isRefreshing) {
browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes)
}
}

Comment on lines 296 to 297
@Override
override fun onProgress(progress: Progress<FileTransferState>) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove unnecessary @Override annotation

The @Override annotation is not used in Kotlin and can lead to compilation errors. The override keyword is sufficient to override methods.

Apply this diff to remove the unnecessary annotation:

-			@Override
			override fun onProgress(progress: Progress<FileTransferState>) {
📝 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
@Override
override fun onProgress(progress: Progress<FileTransferState>) {
override fun onProgress(progress: Progress<FileTransferState>) {

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (5)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (1)

89-98: Remove empty onFastScrollStart method

The onFastScrollStart method is empty and can be safely removed. This will simplify the code without affecting functionality.

Apply this diff to remove the empty method:

 private val onFastScrollStateChangeListener = object : OnFastScrollStateChangeListener {
     @Override
     override fun onFastScrollStop() {
         thumbnailsForVisibleNodes()
     }

-    @Override
-    override fun onFastScrollStart() {
-    }
 }
🧰 Tools
🪛 detekt

[warning] 96-97: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)

Line range hint 387-440: Improve error handling in thumbnail generation process.

The modifications to the read method for incorporating thumbnail generation are generally good. However, there's a potential issue with error handling:

  1. If an exception occurs during the file reading process, the thumbnailWriter and thumbnailReader might not be properly closed.
  2. The futureThumbnail.get() call could potentially block indefinitely if there's an issue with the thumbnail generation thread.

Consider wrapping the thumbnail-related operations in a try-catch block and ensure proper resource cleanup in case of exceptions. Also, consider adding a timeout to the futureThumbnail.get() call to prevent potential deadlocks.

Here's a suggested improvement:

 try {
     // ... existing code ...
 } catch (e: IOException) {
+    closeQuietly(thumbnailWriter)
+    closeQuietly(thumbnailReader)
     throw FatalBackendException(e)
 } finally {
     if (genThumbnail) {
-        futureThumbnail.get()
+        try {
+            futureThumbnail.get(30, TimeUnit.SECONDS)
+        } catch (e: Exception) {
+            Timber.e(e, "Error generating thumbnail")
+        }
     }
 }

Line range hint 1-683: Summary: Good implementation with room for improvement in error handling and resource management.

The changes to CryptoImplDecorator.kt successfully implement thumbnail generation and caching functionality. The code is generally well-structured and efficient. However, there are several areas where improvements can be made:

  1. Error handling: Enhance exception handling in the read method and startThumbnailGeneratorThread to provide more specific error messages and ensure proper resource cleanup.
  2. Resource management: Ensure that all resources (especially InputStreams and temporary files) are properly closed or deleted, even in error scenarios.
  3. Consistency: Use explicit locales in string formatting to ensure consistent behavior across different devices.
  4. Logging: Improve logging to aid in debugging and monitoring, particularly for the closeQuietly method.

Addressing these points will enhance the robustness and maintainability of the code. Overall, the implementation is a solid foundation for the new thumbnail functionality.

🧰 Tools
🪛 detekt

[warning] 449-449: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[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)

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (2)

164-178: New method thumbnailsForVisibleNodes: Looks good with a minor suggestion.

The method correctly implements the logic for identifying visible nodes that need thumbnails and initiates the thumbnail generation process. It appropriately checks user preferences before proceeding.

Consider using filter instead of forEach for a more functional approach:

val toDownload = visibleCloudNodes.filter { node ->
    node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail == null
}

This would make the code more concise and potentially more efficient.


290-312: New method associateThumbnails: Good implementation with a suggestion for optimization.

The method correctly implements the logic for associating thumbnails with cloud nodes, including proper checks for user preferences. However, there's a potential performance optimization in the thumbnail generation part:

images.take(10).filter { img -> img.thumbnail == null }.let { firstImagesWithoutThumbnails ->
    if (firstImagesWithoutThumbnails.isNotEmpty()) {
        thumbnailsForVisibleNodes(firstImagesWithoutThumbnails)
    }
}

This code first takes 10 images and then filters them. It would be more efficient to filter first and then take up to 10 images:

images.filter { img -> img.thumbnail == null }.take(10).let { firstImagesWithoutThumbnails ->
    if (firstImagesWithoutThumbnails.isNotEmpty()) {
        thumbnailsForVisibleNodes(firstImagesWithoutThumbnails)
    }
}

This ensures that we're only processing up to 10 images that actually need thumbnails, potentially reducing unnecessary work.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d85096b and a45edea.

📒 Files selected for processing (3)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (7 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (4 hunks)
🧰 Additional context used
🪛 detekt
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt

[warning] 449-449: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[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)

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt

[warning] 96-97: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

🔇 Additional comments (10)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (3)

12-14: LGTM: Correct imports added for new functionality

The new imports for RecyclerView and SCROLL_STATE_IDLE are appropriate for the added scroll state management functionality. Good job on using the correct constant from RecyclerView instead of AbsListView.


100-108: LGTM: Efficient scroll listener implementation

The new onScrollListener implementation is well-designed. It calls thumbnailsForVisibleNodes() only when the scroll state is idle, which is an efficient approach to loading thumbnails. This prevents unnecessary processing during active scrolling, improving performance.


131-132: LGTM: Proper setup of new listeners

The new listeners onFastScrollStateChangeListener and onScrollListener are correctly added to the RecyclerView in the setupView method. This ensures that the thumbnail loading functionality will work as intended during scrolling and fast scrolling.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)

4-7: LGTM: New imports and member variables for thumbnail functionality.

The added imports and member variables are appropriate for implementing the new thumbnail generation and caching features. The use of a dedicated thread pool for thumbnail generation is a good practice to avoid blocking the main thread.

Also applies to: 31-39, 75-84


489-518: LGTM: Well-implemented associateThumbnails method.

The associateThumbnails method is well-structured and efficient:

  1. It properly checks if thumbnail generation is enabled before proceeding.
  2. The filtering process ensures only relevant files (images without thumbnails) are processed.
  3. The use of measureTimeMillis for performance logging is a good practice for monitoring and future optimizations.
  4. The method handles potential null values appropriately.

The implementation looks good and should perform well.

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (5)

28-28: New imports for thumbnail functionality: LGTM!

The new imports for AssociateThumbnailsUseCase and FileTransferState are correctly added and necessary for the implementation of the thumbnail functionality.

Also applies to: 33-33


102-102: Constructor modification for thumbnail functionality: LGTM!

The addition of the associateThumbnailsUseCase parameter to the constructor is correct and follows the existing pattern of dependency injection.


261-261: Modification to getCloudList method: LGTM!

The addition of associateThumbnails(cloudNodes) after showing cloud nodes in the view is a logical and correct place to perform this operation. It ensures that thumbnails are associated with cloud nodes immediately after retrieving the cloud list.


1407-1408: Modifications to init block: LGTM!

The additions to unsubscribe getDecryptedCloudForVaultUseCase and associateThumbnailsUseCase in the init block are correct and necessary. This ensures proper cleanup of resources when the presenter is destroyed, preventing potential memory leaks and unnecessary background work.


Line range hint 1-1408: Overall assessment of changes in BrowseFilesPresenter.kt

The modifications to implement thumbnail functionality in the BrowseFilesPresenter class are well-designed and properly integrated with the existing codebase. The new methods and changes to existing methods logically implement the required functionality while maintaining consistency with the class's overall structure and coding style.

Key points:

  1. Proper dependency injection for the new use case.
  2. Correct implementation of user preference checks before performing thumbnail operations.
  3. Appropriate error handling and progress reporting in the new methods.
  4. Logical placement of thumbnail association within the existing workflow.

While the implementation is generally solid, a few minor optimizations have been suggested to improve performance and code clarity. These suggestions, if implemented, would further enhance the quality of the code.

In conclusion, the changes effectively implement the desired thumbnail functionality while maintaining good coding practices and consistency with the existing codebase.

Comment on lines 144 to 155
private fun thumbnailsForVisibleNodes() {
val layoutManager = binding.recyclerViewLayout.recyclerView.layoutManager as LinearLayoutManager
val first = layoutManager.findFirstVisibleItemPosition()
val last = layoutManager.findLastVisibleItemPosition()
if (first == NO_POSITION || last == NO_POSITION) {
return
}
val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
if (!binding.swipeRefreshLayout.isRefreshing) {
browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes)
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

LGTM with a minor optimization suggestion

The thumbnailsForVisibleNodes method is well-implemented. It correctly handles edge cases and avoids unnecessary work when the SwipeRefreshLayout is refreshing.

Consider this minor optimization to reduce object creation:

-val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
 if (!binding.swipeRefreshLayout.isRefreshing) {
-    browseFilesPresenter.thumbnailsForVisibleNodes(visibleCloudNodes)
+    browseFilesPresenter.thumbnailsForVisibleNodes(cloudNodesAdapter.renderedCloudNodes(), first, last)
 }

This change avoids creating a new sublist object on each call. You'll need to update the thumbnailsForVisibleNodes method in the BrowseFilesPresenter to accept the full list and the range indices.

Committable suggestion was skipped due to low confidence.

Comment on lines +481 to +483
protected fun generateCacheKey(cryptoFile: CryptoFile): String {
return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
}
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)

Comment on lines 180 to 215
private fun downloadAndGenerateThumbnails(visibleCloudFiles: List<CloudFileModel>) {
view?.showProgress(
visibleCloudFiles, //
ProgressModel(
progressStateModelMapper.toModel( //
DownloadState.download(visibleCloudFiles[0].toCloudNode())
), 0
)
)
downloadFilesUseCase //
.withDownloadFiles(downloadFileUtil.createDownloadFilesFor(this, visibleCloudFiles)) //
.run(object : DefaultProgressAwareResultHandler<List<CloudFile>, DownloadState>() {
override fun onFinished() {
view?.hideProgress(visibleCloudFiles)
}

override fun onProgress(progress: Progress<DownloadState>) {
if (!progress.isOverallComplete) {
view?.showProgress(
cloudFileModelMapper.toModel(progress.state().file()), //
progressModelMapper.toModel(progress)
)
}
if (progress.isCompleteAndHasState) {
val cloudFile = progress.state().file()
val cloudFileModel = cloudFileModelMapper.toModel(cloudFile)
view?.addOrUpdateCloudNode(cloudFileModel)
}
}

override fun onError(e: Throwable) {
view?.hideProgress(visibleCloudFiles)
super.onError(e)
}
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

New method downloadAndGenerateThumbnails: Good implementation with a minor concern.

The method correctly implements the download and generation of thumbnails, including proper progress reporting and view updates. However, there's a potential issue with error handling:

In the onError callback, after calling super.onError(e), the method continues to hide the progress. This might lead to inconsistent UI state if the superclass method throws an exception or performs some critical error handling.

Consider refactoring the error handling like this:

override fun onError(e: Throwable) {
    view?.hideProgress(visibleCloudFiles)
    super.onError(e)
}

This ensures that the progress is always hidden before any potential exception in the superclass method.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
presentation/src/main/res/values/strings.xml (1)

673-673: Follow consistent naming convention for toast messages.

The toast message string key doesn't follow the consistent naming pattern used in the file. Other toast messages use _toast as a suffix rather than a prefix.

Apply this diff to follow the consistent naming pattern:

-	<string name="thumbnail_generation__deactivation_toast">LRU cache disabled therefore also the thumbnails</string>
+	<string name="thumbnail_generation_deactivation_toast">LRU cache disabled therefore also the thumbnails</string>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a45edea and bdc28c2.

📒 Files selected for processing (1)
  • presentation/src/main/res/values/strings.xml (2 hunks)
🔇 Additional comments (1)
presentation/src/main/res/values/strings.xml (1)

684-684: LGTM!

The dialog title string resource is well-named and follows the consistent naming pattern used throughout the file.

Comment on lines +669 to +670
<string name="thumbnail_generation_never">Never</string>
<string name="thumbnail_generation_readonly">Read Only</string>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use string references for duplicate values.

These strings are duplicates of existing values. Consider referencing the existing strings instead of duplicating them.

Apply this diff to reference existing strings:

-	<string name="thumbnail_generation_never">Never</string>
-	<string name="thumbnail_generation_readonly">Read Only</string>
+	<string name="thumbnail_generation_never" translatable="false">@string/lock_timeout_never</string>
+	<string name="thumbnail_generation_readonly">Read Only</string>

Committable suggestion skipped: line range outside the PR's diff.

@alejandracios
Copy link

Wondering when thumbnails will be available for the Android version. App is excellent but not being able to see file in a glance is kind of a fail.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (1)

80-100: Persist/restore the thumbnail preference state instead of only toggling UI availability

When the LRU cache is disabled (newValue == FALSE) the code makes the thumbnail generation preference non-interactive, but it does not persist a new value (e.g. switch to ThumbnailsOption.NEVER).
After a process death the user will therefore see the preference re-enabled while the cache is still disabled, leading to inconsistent behaviour inside BrowseFilesPresenter.

Consider:

  1. Persisting a sane default (ThumbnailsOption.NEVER) when disabling the cache.
  2. Re-reading the persisted value and re-initialising the UI when the cache is re-enabled (still TODO: setupThumbnailGeneration()).

This was already raised in an earlier review – please avoid regressing.

🧹 Nitpick comments (3)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (1)

341-342: Nit: give constants a clearer name

THUMBNAIL_GENERATION is a key, not the preference itself.
A more explicit name (KEY_THUMBNAIL_GENERATION) would increase readability and remain consistent with the other constants in this companion object.

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (2)

164-178: Throttle repeated thumbnail downloads to avoid redundant network/IO work

thumbnailsForVisibleNodes is called every time the view finishes associateThumbnails().
On fast scrolling this will very likely be triggered repeatedly with overlapping sets, causing:

  • duplicate downloads of the same image,
  • unneeded cache churn,
  • increased battery/network usage.

Consider memoising the set of already–requested thumbnails (e.g. via a HashSet<String> keyed by cloud id+revision) or cancelling an in-flight downloadFilesUseCase before starting a new one.


180-215: Method name is misleading – thumbnails are not generated here

downloadAndGenerateThumbnails triggers only the download; the actual thumbnail creation is delegated elsewhere.
To avoid confusion for future maintainers (and incorrect assumptions in tests), either:

  1. Rename the method to downloadVisibleImages(), or
  2. Trigger thumbnail generation explicitly after the download finishes (e.g. invoke associateThumbnailsUseCase for the fresh files).
- private fun downloadAndGenerateThumbnails(visibleCloudFiles: List<CloudFileModel>) {
+ private fun downloadVisibleImages(visibleCloudFiles: List<CloudFileModel>) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bdc28c2 and 340338a.

📒 Files selected for processing (4)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (7 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt (3 hunks)
  • presentation/src/main/res/values/strings.xml (2 hunks)
  • presentation/src/main/res/xml/preferences.xml (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • presentation/src/main/res/xml/preferences.xml
  • presentation/src/main/res/values/strings.xml

Comment on lines +85 to +94
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = false
}
Toast.makeText(context, context?.getString(R.string.thumbnail_generation__deactivation_toast), Toast.LENGTH_LONG).show()
}

if (TRUE == newValue) {
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = true
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prefer isEnabled over isSelectable for run-time toggling of a Preference

Preference#setSelectable(Boolean) only exists from androidx.preference 1.2.0 upwards and is still marked @RestrictTo(LIBRARY).
Using it in production code can therefore break builds for callers that are still on 1.1.x (which is what many OEM images ship) and will result in a compile-time error.
The public and backwards-compatible API for disabling user interaction is Preference#setEnabled(Boolean).

- preference.isSelectable = false
+ preference.isEnabled = false- preference.isSelectable = true
+ preference.isEnabled = true
📝 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
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = false
}
Toast.makeText(context, context?.getString(R.string.thumbnail_generation__deactivation_toast), Toast.LENGTH_LONG).show()
}
if (TRUE == newValue) {
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
preference.isSelectable = true
}
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
- preference.isSelectable = false
+ preference.isEnabled = false
}
Toast.makeText(
context,
context?.getString(R.string.thumbnail_generation__deactivation_toast),
Toast.LENGTH_LONG
).show()
}
if (TRUE == newValue) {
findPreference<ListPreference>(THUMBNAIL_GENERATION)?.let { preference ->
- preference.isSelectable = true
+ preference.isEnabled = true
}

Prioritize the visible area for downloading the files and generating the thumbnails. Add MAX_CONCURRENT_THUMBNAILS_THREADS to limit the number of usecase-threads generated. Do not start a double download of the same file and, finally, remove the progress bars for dowloads and substitute them with simple icons to lighten th UI.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (2)

89-98: Remove empty onFastScrollStart method.

The onFastScrollStart method is empty and can be removed since no action is needed when fast scrolling starts.

Apply this diff to remove the empty method:

override fun onFastScrollStop() {
    thumbnailsForVisibleNodes()
}

-override fun onFastScrollStart() {
-}
🧰 Tools
🪛 detekt (1.23.7)

[warning] 96-97: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)


144-155: Handle edge cases in thumbnailsForVisibleNodes.

The method correctly checks for NO_POSITION to avoid IndexOutOfBoundsException. However, creating a new sublist on each call could be optimized.

Consider this optimization to reduce object creation:

-val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1)
if (!binding.swipeRefreshLayout.isRefreshing) {
-   browseFilesPresenter.associateThumbnails(visibleCloudNodes)
+   browseFilesPresenter.associateThumbnails(cloudNodesAdapter.renderedCloudNodes(), first, last)
}

This would require updating the associateThumbnails method in BrowseFilesPresenter to accept the full list and range indices.

🧹 Nitpick comments (1)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (1)

150-152: Consider validating media type more thoroughly.

The current implementation checks if the media type's main type is "image", which is correct. However, it relies on the extension to determine the media type and uses a fallback wildcard type. For more robust handling, you might consider additional checks for specific image formats that are supported by BitmapFactory.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 340338a and 24c9747.

📒 Files selected for processing (5)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (11 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (4 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (3)
presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt (1)
  • replaceImageWithDownloadIcon (39-39)
presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt (1)
  • replaceImageWithDownloadIcon (544-546)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (1)
  • replaceImageWithDownloadIcon (335-337)
🪛 detekt (1.23.7)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt

[warning] 96-97: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

🔇 Additional comments (7)
presentation/src/main/java/org/cryptomator/presentation/ui/activity/view/BrowseFilesView.kt (1)

39-40: Interface extension for thumbnail download state updates.

These new methods provide a clean way for the presenter to communicate thumbnail download states to the view, enabling UI updates when thumbnails are being downloaded. Good design choice to separate the single and batch update operations.

presentation/src/main/java/org/cryptomator/presentation/ui/activity/BrowseFilesActivity.kt (1)

544-550: Proper implementation of new interface methods.

The implementation correctly delegates to the corresponding fragment methods, maintaining the separation of concerns in the MVP architecture. This follows the established pattern in other similar methods in this class.

presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (2)

3-3: Added dependencies for thumbnail support.

The BitmapFactory and MimeTypes imports, along with the constructor injection of MimeTypes, properly support the new thumbnail functionality.

Also applies to: 34-35, 43-44


335-337: Clean implementation for download icon replacement.

The method is concise and focused on a single responsibility, making it easy to reuse across different contexts.

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (3)

100-108: Efficient scroll listener implementation.

The scroll listener correctly triggers thumbnail association only when scrolling has stopped (SCROLL_STATE_IDLE), which improves performance by avoiding unnecessary processing during active scrolling.


131-132: Proper attachment of scroll listeners.

The scroll listeners are correctly attached to the RecyclerView during setup, ensuring they start functioning immediately when the fragment is displayed.


241-252: Well-implemented image replacement methods.

These methods properly handle the case where a ViewHolder isn't found for a node, avoiding potential null pointer exceptions. The implementation follows the same pattern as other similar methods in this class (like hideProgress and showProgress).

aiya000 added 3 commits August 5, 2025 22:00
- Enable largeHeap in AndroidManifest.xml for increased memory allocation
- Implement dynamic sampling in thumbnail generation based on file size:
* 50MB+: 1/256 sampling, 30MB+: 1/144 sampling, etc.
- Use RGB_565 bitmap config and proper bitmap.recycle() for memory efficiency
- Fix pipe synchronization issues between decryption and thumbnail generation
- Handle "Pipe closed" errors gracefully without showing fatal error toast
- Add proper OutOfMemoryError handling with specific error messages
- Add timeout mechanism for thumbnail generation to prevent hanging

This resolves the "An error occurred" toast that appeared when opening
large image files with thumbnail generation enabled.
- Convert CloudFileModel.thumbnail to dynamic property to reflect changes after thumbnail generation
- Add thumbnail re-association when returning from image preview in BrowseFilesPresenter.resumed()
- Ensure thumbnails display correctly both on initial folder load and when navigating back from image view
- Remove redundant refresh logic and clean up code
…rror

Fix/feature thumbnail generation error
@aiya000
Copy link

aiya000 commented Aug 21, 2025

@SailReal CC: @JustFanta01

I would like to inform you ✨
I have discovered issues with this PR's branch, so I have forked JustFanta01's fork and am currently working on it here:

https://github.com/aiya000/cryptomator-android

The work I'm doing first is:

  • Large image files fail to generate thumbnails and display errors. Photos cannot be displayed

And I've submitted a PR to JustFanta01 for this:

JustFanta01#8

Anticipating that there might be no response from JustFanta01 to this PR (#533) and my above PR, I contacted you early.
Please forgive my rudeness!

Additionally, I plan to continue working on the tasks I've logged here:

https://github.com/aiya000/cryptomator-android/issues


So, if JustFanta01 seems to be busy, would you both be willing to let me take over this PR (#533)?

If this is possible, I would appreciate it if you could let me know the current state!
Is it at a stage that requires refactoring?

Also, personally, I feel that thumbnail generation is always performed when accessing photos, even if they have already been generated.
(Though I haven't investigated this accurately yet...)

Taking these into account, I would be happy to discuss how to proceed with both of you 🙌

@aiya000
Copy link

aiya000 commented Aug 21, 2025

NOTE: I'm also implmenting Grid View now.

@SailReal
Copy link
Member

@aiya000 It would be awesome if you could continue working on thumbnails and grid view 💚

Please send us a quick message at [email protected] so that we can add you to our development channel in our chat app. We can then discuss the current state of this feature and how we proceed.

@WheelyMcBones
Copy link
Contributor

Hi @aiya000
Responding on behalf of @JustFanta01 and @taglioIsCoding too.

First of all, thank you for your contribution! We are quite busy in this period, but we will do our best to review your PR in the next days.

Then, we would be glad to continue working on these open PRs together with you.

Thanks

cc: @SailReal

@aiya000
Copy link

aiya000 commented Sep 5, 2025

@aiya000 It would be awesome if you could continue working on thumbnails and grid view 💚

Please send us a quick message at [email protected] so that we can add you to our development channel in our chat app. We can then discuss the current state of this feature and how we proceed.

@SailReal

Thank you :D
I'll send a quick message!

@aiya000
Copy link

aiya000 commented Sep 5, 2025

Hi @aiya000 Responding on behalf of @JustFanta01 and @taglioIsCoding too.

First of all, thank you for your contribution! We are quite busy in this period, but we will do our best to review your PR in the next days.

Then, we would be glad to continue working on these open PRs together with you.

Thanks

cc: @SailReal

@WheelyMcBones

Thank you so much for replying!
I'm glad :D.

Yes, I will do that.
First, wait for the PR review I sent to @JustFanta01 , then send a mail that @SailReal said.
I would be happy to discuss how to proceed!

…-pictures

Resolve generating thumbnails of larger pictures
@CLAassistant
Copy link

CLAassistant commented Oct 19, 2025

CLA assistant check
All committers have signed the CLA.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (3)

460-466: Log and swallow in closeQuietly instead of silent ignore.

Silent ignore hinders debugging. Log at debug level. (detekt: SwallowedException)

 private fun closeQuietly(closeable: Closeable) {
     try {
-        closeable.close();
+        closeable.close()
     } catch (e: IOException) {
-        // ignore
+        Timber.d(e, "IOException while closing resource")
     }
 }

539-541: Use explicit Locale for cache key formatting.

Avoid implicit default locale. (detekt: ImplicitDefaultLocale)

-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(java.util.Locale.US, "%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode())
+}

582-595: Fix potential resource leak when writing thumbnail; always close and delete temp.

Use use() and ensure deletion in finally.

 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()
+    val thumbnailFile = File.createTempFile(UUID.randomUUID().toString(), ".thumbnail", internalCache)
+    try {
+        thumbnailFile.outputStream().use { out ->
+            // Consider a lower quality (e.g., 85) to reduce cache size
+            thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 85, out)
+        }
+        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")
+        }
+    } finally {
+        thumbnailFile.delete()
+    }
 }
🧹 Nitpick comments (11)
presentation/src/main/AndroidManifest.xml (1)

35-35: largeHeap should be a last resort; verify necessity with profiling.

largeHeap can mask leaks and reduce system health. Given you already use tiling (SubsamplingScaleImageView), prefer downsampling/streaming over process-wide heap increases. If kept, please document the measured memory class, typical image sizes, and why alternatives weren’t sufficient.

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/ImagePreviewFragment.kt (1)

86-100: Don’t swallow OOM; log and use a dedicated error path.

Good SSIV config. However, the OOM is swallowed and routed via onImagePreviewClicked(), which conflates UX paths and loses the throwable (detekt warning). Log the error and call a dedicated presenter method (e.g., onImagePreviewLoadFailed) or show the error directly.

Apply minimal change (keep current flow but add logging):

@@
-                imagePreviewFile.uri?.let {
-                    try {
-                        imageView.setImage(ImageSource.uri(it))
-                    } catch (e: OutOfMemoryError) {
-                        // Handle OOM gracefully
-                        presenter.onImagePreviewClicked() // This will show an error through the presenter
-                    }
-                }
+                imagePreviewFile.uri?.let {
+                    try {
+                        imageView.setImage(ImageSource.uri(it))
+                    } catch (e: OutOfMemoryError) {
+                        Timber.e(e, "OOM while loading image preview: %s", it)
+                        presenter.onImagePreviewClicked()
+                    }
+                }

Add import outside this hunk:

import timber.log.Timber
presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt (1)

14-15: Simplify getter with safe-cast.

Avoid double toCloudNode() and explicit cast; use safe-cast.

-    val thumbnail : File? 
-        get() = if (toCloudNode() is CryptoFile) (toCloudNode() as CryptoFile).thumbnail else null
+    val thumbnail: File?
+        get() = (toCloudNode() as? CryptoFile)?.thumbnail
presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt (1)

76-87: Replace brittle message heuristics with cause-chain check and log the exception.

String-matching on “memory/large” is fragile and locale-dependent. Inspect the cause chain for OutOfMemoryError and always log the exception. Consider relying on this single catch and removing the separate OOM catch for simplicity.

-        } catch (e: FatalBackendException) {
-            // Check if it's a memory issue with large images
-            if (e.message?.contains("memory", ignoreCase = true) == true ||
-                e.cause is OutOfMemoryError ||
-                e.message?.contains("large", ignoreCase = true) == true) {
-                showError(getString(R.string.error_image_too_large))
-            } else {
-                showError(getString(R.string.error_generic))
-            }
-            finish()
-        } catch (e: OutOfMemoryError) {
-            showError(getString(R.string.error_image_too_large))
-            finish()
-        }
+        } catch (e: FatalBackendException) {
+            Timber.e(e, "Failed to set up image preview")
+            if (e.hasCause<OutOfMemoryError>()) {
+                showError(getString(R.string.error_image_too_large))
+            } else {
+                showError(getString(R.string.error_generic))
+            }
+            finish()
+        } catch (e: OutOfMemoryError) {
+            Timber.e(e, "OOM while setting up image preview")
+            showError(getString(R.string.error_image_too_large))
+            finish()
+        }

Add helper and import outside this hunk:

import timber.log.Timber

private inline fun <reified T : Throwable> Throwable.hasCause(): Boolean {
    var c: Throwable? = this
    while (c != null) {
        if (c is T) return true
        c = c.cause
    }
    return false
}
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (2)

209-216: Consider scaling the re-association batch to viewport size.

Hard-coding 20 may waste work on small screens and be insufficient on large grids. Consider deriving the count from visible cell capacity.


251-255: Align initial association batch with UI capacity.

Similar to resumed(), consider basing take(10) on visible cells.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (5)

81-85: Bound the thumbnail executor to avoid unbounded thread growth.

newCachedThreadPool may spin up many threads under load. Prefer a small fixed pool.

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

489-493: Remove deprecated BitmapFactory options.

inPurgeable/inInputShareable are deprecated/no-ops and can mislead. Rely on inPreferredConfig and sampling.

-  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
+  options.inPreferredConfig = Bitmap.Config.RGB_565
+  options.inDither = false

524-525: Avoid calling System.gc().

Forcing GC is discouraged and rarely helpful; remove it.

-        // Try to recover by forcing garbage collection
-        System.gc()
+        // Let the runtime handle memory pressure naturally

503-516: Guard extractThumbnail() and recycling; minor nits.

You already handle null decode. Also consider skipping extractThumbnail if bitmap is already <= target size to save work.


440-447: Make the wait timeout configurable (optional).

5s might be short for very large images/slow devices. Consider a higher timeout or a setting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24c9747 and 2271139.

📒 Files selected for processing (8)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7 hunks)
  • presentation/src/main/AndroidManifest.xml (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (12 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt (1 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/fragment/ImagePreviewFragment.kt (1 hunks)
  • presentation/src/main/res/values-ja-rJP/strings.xml (1 hunks)
  • presentation/src/main/res/values/strings.xml (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • presentation/src/main/res/values/strings.xml
🧰 Additional context used
🧬 Code graph analysis (4)
presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt (1)
presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt (1)
  • toCloudNode (27-29)
presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt (1)
presentation/src/main/java/org/cryptomator/presentation/ui/activity/BaseActivity.kt (3)
  • showError (290-298)
  • showError (300-307)
  • finish (285-288)
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (5)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)
  • isImageMediaType (597-599)
  • associateThumbnails (547-576)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (1)
  • isImageMediaType (150-152)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt (1)
  • associateThumbnails (99-102)
data/src/main/java/org/cryptomator/data/repository/DispatchingCloudContentRepository.kt (1)
  • associateThumbnails (168-180)
domain/src/main/java/org/cryptomator/domain/repository/CloudContentRepository.kt (1)
  • associateThumbnails (98-101)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (1)
  • isImageMediaType (638-640)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (1)
  • isImageMediaType (150-152)
🪛 detekt (1.23.8)
presentation/src/main/java/org/cryptomator/presentation/ui/fragment/ImagePreviewFragment.kt

[warning] 96-96: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

presentation/src/main/java/org/cryptomator/presentation/ui/activity/ImagePreviewActivity.kt

[warning] 85-85: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt

[warning] 441-441: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 463-463: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


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

(detekt.potential-bugs.ImplicitDefaultLocale)

🔇 Additional comments (1)
presentation/src/main/res/values-ja-rJP/strings.xml (1)

7-7: LGTM: translation added for oversized-image error.

Comment on lines +391 to +401
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)
}
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.

Comment on lines +140 to +142
private var availableThumbnailsThreads = MAX_CONCURRENT_THUMBNAILS_THREADS
private val filesBeingDownloaded: MutableSet<CloudFileModel> = mutableSetOf()

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

🧩 Analysis chain

Prevent stuck downloads and leaked “thumbnail threads”; track in-flight by stable key and release on errors.

  • Release the baton on errors, not only in onFinished.
  • filesBeingDownloaded currently stores CloudFileModel objects but removes via newly mapped instances; if equals/hashCode differ, removal fails → perpetual “already downloading” skips.
  • Track by a stable key (e.g., node.toCloudNode().path) and always remove on error.
-  private val filesBeingDownloaded: MutableSet<CloudFileModel> = mutableSetOf()
+  // Track by stable key to avoid equality mismatches
+  private val filesBeingDownloaded: MutableSet<String> = mutableSetOf()
 private fun downloadAndGenerateThumbnails(visibleCloudFiles: List<CloudFileModel>) {
-    filesBeingDownloaded.addAll(visibleCloudFiles)
+    filesBeingDownloaded.addAll(visibleCloudFiles.map { it.toCloudNode().path })
     view?.replaceImagesWithDownloadIcon(
         visibleCloudFiles
     )
     downloadFilesUseCase //
       .withDownloadFiles(downloadFileUtil.createDownloadFilesFor(this, visibleCloudFiles)) //
       .run(object : DefaultProgressAwareResultHandler<List<CloudFile>, DownloadState>() {
         override fun onFinished() {
           availableThumbnailsThreads++ // releasing the passed baton
           Timber.tag("THUMBNAILS").i("[RELEASE] downloadAndGen (${availableThumbnailsThreads}/${MAX_CONCURRENT_THUMBNAILS_THREADS})")
         }
 
         override fun onProgress(progress: Progress<DownloadState>) {
           if (progress.isCompleteAndHasState) {
             val cloudFile = progress.state().file()
             val cloudFileModel = cloudFileModelMapper.toModel(cloudFile)
-            filesBeingDownloaded.remove(cloudFileModel)
+            filesBeingDownloaded.remove(cloudFileModel.toCloudNode().path)
             view?.addOrUpdateCloudNode(cloudFileModel)
           }
         }
 
         override fun onError(e: Throwable) {
-          view?.hideProgress(visibleCloudFiles)
+          view?.hideProgress(visibleCloudFiles)
+          // Ensure cleanup on failure
+          filesBeingDownloaded.removeAll(visibleCloudFiles.map { it.toCloudNode().path })
+          availableThumbnailsThreads = (availableThumbnailsThreads + 1).coerceAtMost(MAX_CONCURRENT_THUMBNAILS_THREADS)
+          Timber.tag("THUMBNAILS").i("[RELEASE-ERR] downloadAndGen (${availableThumbnailsThreads}/${MAX_CONCURRENT_THUMBNAILS_THREADS})")
           super.onError(e)
         }
       })
 }
   cloudNodes.filter { node -> !associatedCloudNodes.contains(node) }.forEach { node ->
     if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail == null) {
-      if (filesBeingDownloaded.contains(node)) {
+      if (filesBeingDownloaded.contains(node.toCloudNode().path)) {
         Timber.tag("THUMBNAILS").i("[SKIP] No double download!")
       } else {
         toDownload.add(node)
       }
     }
   }
 fun onBackPressed() {
   unsubscribeAll()
   Timber.tag("THUMBNAILS").i("[RESET] unsubscribe to all")
   availableThumbnailsThreads = MAX_CONCURRENT_THUMBNAILS_THREADS
-  filesBeingDownloaded.clear()
+  filesBeingDownloaded.clear()
 }
 
 fun onFolderClicked(cloudFolderModel: CloudFolderModel) {
   unsubscribeAll()
   Timber.tag("THUMBNAILS").i("[RESET] unsubscribe to all")
   availableThumbnailsThreads = MAX_CONCURRENT_THUMBNAILS_THREADS
-  filesBeingDownloaded.clear()
+  filesBeingDownloaded.clear()
   view?.navigateTo(cloudFolderModel)
 }

To be safe, confirm equality semantics to justify the refactor:

Also applies to: 167-194, 321-329, 228-229, 1001-1003


🏁 Script executed:

#!/bin/bash
# Inspect CloudFileModel to see if it's a data class or overrides equals/hashCode by path/ID.
rg -nC2 -g '!**/build/**' -P 'data\s+class\s+CloudFileModel\b|class\s+CloudFileModel\b|override\s+fun\s+equals|override\s+fun\s+hashCode'

Length of output: 12212



---

🏁 Script executed:

```shell
#!/bin/bash
# Find internalEquals implementation in CloudModel and CloudNodeModel
rg -nA5 -P 'fun\s+internalEquals' --type=kt -g '!**/build/**'

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check CloudNodeModel and CloudModel full definitions to see internalEquals
rg -nA30 'class CloudNodeModel|class CloudModel' --type=kt -g '!**/build/**' | grep -A30 'presentation/src/main/java/org/cryptomator/presentation/model'

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Also check if there are comments or patterns showing what internalEquals checks
rg -nB2 -A3 'internalEquals' --type=kt -g '!**/build/**' presentation/src/main/java/org/cryptomator/presentation/model/

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Verify CloudFile equality/hashCode to understand the base equality
rg -nC3 'class CloudFile|data class CloudFile|override fun equals|override fun hashCode' --type=kt -g '!**/build/**' domain/ | head -50

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check if CloudFileModel.icon field affects equals (look for its usage in CloudNodeModel/CloudModel)
rg -nP '\bicon\b' --type=kt presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Search for the actual CloudNodeModel implementation to see internalEquals
cat presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt

Length of output: 1122


🏁 Script executed:

#!/bin/bash
# Search for CloudModel implementation
cat presentation/src/main/java/org/cryptomator/presentation/model/CloudModel.kt

Length of output: 683


🏁 Script executed:

#!/bin/bash
# Check if CloudFile is data class
rg -nB2 'class CloudFile|data class CloudFile' --type=kt domain/ | head -20

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Look at the structure to understand path availability
rg -n 'fun\s+.*path|val\s+path' --type=kt presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check CloudNodeModel parent to see if toCloudNode().path is valid as suggested
rg -nC2 'fun toCloudNode|val cloudNode' --type=kt presentation/src/main/java/org/cryptomator/presentation/model/CloudNodeModel.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Verify the review comment's assumption about toCloudNode().path
rg -n 'toCloudNode\(\)' --type=kt presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt | head -5

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check if toCloudNode is defined and what it returns
rg -nA3 'fun toCloudNode' --type=kt presentation/src/main/java/org/cryptomator/presentation/model/

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Final check: does CloudFile/CloudNode have a path property?
rg -nP 'val\s+path\s*:|var\s+path\s*:' --type=kt domain/ | grep -i 'cloudfile\|cloudnode' | head -10

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Look for path in CloudNode interfaces/classes
rg -nC2 'interface CloudNode|class CloudNode|data class CloudNode' --type=kt domain/

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Simpler: find what toCloudNode returns for CloudFileModel
ast-grep --pattern 'fun toCloudNode(): $_ { $$$ }'

Length of output: 294


🏁 Script executed:

#!/bin/bash
# Let's check the domain module for CloudFile/CloudNode definitions
find . -path '*/domain/*' -name '*.kt' | grep -i 'cloud' | head -20

Length of output: 873


🏁 Script executed:

#!/bin/bash
# Get the full BrowseFilesPresenter.kt to see the actual implementation of error handling
cat presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt

Length of output: 50907


🏁 Script executed:

#!/bin/bash
# Search for cloudFileModelMapper to see how it creates models
find . -name '*cloudFileModelMapper*' -o -name '*CloudFileModelMapper*' 2>/dev/null

Length of output: 95


🏁 Script executed:

#!/bin/bash
# Look for mapper implementation
rg -n 'cloudFileModelMapper|CloudFileModelMapper' presentation/src/main/java/org/cryptomator/presentation/ --max-count=20

Length of output: 4200


🏁 Script executed:

#!/bin/bash
# Check CloudFile to see if it's a data class or has proper equality
cat domain/src/main/java/org/cryptomator/domain/CloudFile.kt

Length of output: 163


🏁 Script executed:

#!/bin/bash
# Check if CloudNode has proper equality
cat domain/src/main/java/org/cryptomator/domain/CloudNode.kt

Length of output: 181


Release thumbnail baton on download errors and clean up in-flight tracking.

The onError handler in downloadAndGenerateThumbnails() is missing critical cleanup. When a download fails:

  1. Baton is not released: availableThumbnailsThreads++ is missing, leaving the semaphore permanently held at 0 and blocking all future thumbnail downloads.
  2. In-flight entries are not cleared: Failed downloads remain in filesBeingDownloaded, causing subsequent checks via contains() to always skip those files.
  3. Equality risk: Adding/removing CloudFileModel via newly mapped instances may fail if equality doesn't align with mapper output.

Switching to path-based tracking (String) eliminates mapper/equality coupling and makes cleanup unambiguous.

-	private val filesBeingDownloaded: MutableSet<CloudFileModel> = mutableSetOf()
+	private val filesBeingDownloaded: MutableSet<String> = mutableSetOf()
 private fun downloadAndGenerateThumbnails(visibleCloudFiles: List<CloudFileModel>) {
-	filesBeingDownloaded.addAll(visibleCloudFiles)
+	filesBeingDownloaded.addAll(visibleCloudFiles.map { it.path })
 				override fun onProgress(progress: Progress<DownloadState>) {
 					if (progress.isCompleteAndHasState) {
 						val cloudFile = progress.state().file()
 						val cloudFileModel = cloudFileModelMapper.toModel(cloudFile)
-						filesBeingDownloaded.remove(cloudFileModel)
+						filesBeingDownloaded.remove(cloudFileModel.path)
 						view?.addOrUpdateCloudNode(cloudFileModel)
 					}
 				}

 				override fun onError(e: Throwable) {
 					view?.hideProgress(visibleCloudFiles)
+					filesBeingDownloaded.removeAll(visibleCloudFiles.map { it.path })
+					availableThumbnailsThreads = (availableThumbnailsThreads + 1).coerceAtMost(MAX_CONCURRENT_THUMBNAILS_THREADS)
+					Timber.tag("THUMBNAILS").i("[RELEASE-ERR] downloadAndGen (${availableThumbnailsThreads}/${MAX_CONCURRENT_THUMBNAILS_THREADS})")
 					super.onError(e)
 				}
 				cloudNodes.filter { node -> !associatedCloudNodes.contains(node) }.forEach { node ->
 					if (node is CloudFileModel && isImageMediaType(node.name) && node.thumbnail == null) {
-						if (filesBeingDownloaded.contains(node)) {
+						if (filesBeingDownloaded.contains(node.path)) {
 							Timber.tag("THUMBNAILS").i("[SKIP] No double download!")
 						} else {
 							toDownload.add(node)
 						}
 					}
 				}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt
around lines 140-142, the onError handler for downloadAndGenerateThumbnails
fails to release the thumbnail "baton" and to clear in-flight tracking, and it
relies on CloudFileModel equality which may not match mapper instances; update
the error path to (1) increment availableThumbnailsThreads (or call the existing
release method) to free the semaphore, (2) remove the file from the in-flight
tracking collection, and (3) switch filesBeingDownloaded from CloudFileModel to
tracking by unique path String (use model.path or equivalent) so add/remove
always target the same key; ensure these operations are performed
atomically/thread-safely (synchronize or use concurrent set) and mirror the same
cleanup in both success and error handlers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)

535-537: Specify explicit locale in String.format for cache key generation.

Using the implicit default locale can cause cache key inconsistencies across devices. This was flagged by static analysis and multiple previous reviews.

Apply this diff:

+import java.util.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())
 }

As per static analysis and coding guidelines.


393-394: Create pipes lazily to avoid unnecessary resource allocation.

The pipes are created unconditionally even when genThumbnail is false, wasting resources. A previous critical review flagged this issue and suggested lazy initialization.

Apply this diff to create pipes only when needed:

-		val thumbnailWriter = PipedOutputStream()
-		val thumbnailReader = PipedInputStream(thumbnailWriter)
+		var thumbnailWriter: PipedOutputStream? = null
+		var thumbnailReader: PipedInputStream? = null

Then, after line 398, create the pipes conditionally:

 		val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)
 
 		if (genThumbnail) {
+			thumbnailWriter = PipedOutputStream()
+			thumbnailReader = PipedInputStream(thumbnailWriter)
 			futureThumbnail = startThumbnailGeneratorThread(cryptoFile, diskCache!!, cacheKey, thumbnailReader)
 		}

Update line 416 to use null-safe call:

-				thumbnailWriter.write(buff.array(), 0, buff.remaining())
+				thumbnailWriter?.write(buff.array(), 0, buff.remaining())

Update line 442 to use null-safe call:

-			closeQuietly(thumbnailWriter)
+			thumbnailWriter?.let { closeQuietly(it) }

Based on past reviews.

🧹 Nitpick comments (7)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (2)

141-152: Good: Null bitmap check now prevents crashes.

The null check on lines 144–147 correctly addresses the previous critical issue where BitmapFactory.decodeFile could return null for unsupported formats like SVG or corrupted files.

One optional refinement: BitmapFactory.decodeFile without options may load the entire bitmap into memory. If thumbnails are unexpectedly large, consider using BitmapFactory.Options with bounds decoding to constrain memory usage.


154-156: Extract isImageMediaType to eliminate duplication.

This helper is duplicated identically in CryptoImplDecorator.kt (lines 622–624) and BrowseFilesPresenter.kt (lines 637–639). Extract it to a shared utility class or extension function on MimeTypes to maintain a single source of truth.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (5)

414-420: Consider tracking pipe failure state to avoid repeated error logging.

The error handling correctly prevents pipe failures from aborting decryption. However, if the write fails once, subsequent iterations will continue attempting writes and logging warnings. Consider introducing a flag to skip further pipe operations after the first failure.

Apply this diff:

+		var thumbnailPipeOpen = genThumbnail
+
 		try {
 			val encryptedTmpFile = readToTmpFile(cryptoFile, ciphertextFile, progressAware)

Then update the write logic:

-			if (genThumbnail) {
+			if (thumbnailPipeOpen) {
 				try {
-					thumbnailWriter.write(buff.array(), 0, buff.remaining())
+					thumbnailWriter?.write(buff.array(), 0, buff.remaining())
 				} catch (e: IOException){
 					Timber.w(e, "Failed to write thumbnail to output stream: ${cryptoFile.name} - skipping thumbnail generation")
+					closeQuietly(thumbnailWriter!!)
+					thumbnailPipeOpen = false
 				}
 			}

Also note: Line 433's thumbnailWriter.flush() will need null-safety if pipes are made nullable.


459-465: LGTM: Exception logging added as requested.

The method now logs IOExceptions at debug level, addressing previous review feedback.

Minor nitpick: The semicolon on line 461 is unnecessary in Kotlin.

Apply this diff to remove the unnecessary semicolon:

-		closeable.close();
+		closeable.close()

467-533: Strong OOM handling but consider consolidating cleanup logic.

The adaptive sampling and explicit bitmap recycling effectively prevent OOM errors for large images. The error handling is comprehensive with appropriate logging.

However, closeQuietly(thumbnailReader) is called in multiple catch blocks (lines 494, 517, 522, 529). Consider consolidating this into a finally block:

 	private fun startThumbnailGeneratorThread(cryptoFile: CryptoFile, diskCache: DiskLruCache, cacheKey: String, thumbnailReader: PipedInputStream): Future<*> {
 		return thumbnailExecutorService.submit {
 			try {
 				val options = BitmapFactory.Options()
 				val thumbnailBitmap: Bitmap?
 
 				// ... sampling logic ...
 
 				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
 				}
 
 				// ... thumbnail extraction and storage ...
 
 				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)")
 				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}")
+			} finally {
+				closeQuietly(thumbnailReader)
 			}
 		}
 	}

Note: System.gc() on line 520 is generally discouraged as it's only a hint, but it's acceptable here as a last-resort recovery attempt after OOM.


608-621: Improve resource management in temporary file handling.

The temporary file cleanup at line 620 will not execute if an exception occurs during bitmap compression (line 610), and the output stream is not explicitly closed.

Apply this diff:

 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.outputStream().use { outputStream ->
+			thumbnailBitmap.compress(Bitmap.CompressFormat.JPEG, 100, 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")
+		}
+	} finally {
+		thumbnailFile.delete()
 	}
-
-	thumbnailFile.delete()
 }

This ensures the temporary file is always deleted and the output stream is properly closed.


623-625: Extract duplicated method to shared utility.

This method is duplicated in at least three locations:

  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (lines 623-625)
  • presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (lines 153-155)
  • presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (lines 637-639)

Consider extracting this to a shared utility class (e.g., MimeTypeUtils or extending MimeTypes) to avoid duplication:

// In a shared utility file
object MimeTypeUtils {
    fun isImageMediaType(mimeTypes: MimeTypes, filename: String): Boolean {
        return (mimeTypes.fromFilename(filename) ?: MimeType.WILDCARD_MIME_TYPE).mediatype == "image"
    }
}

Then use this shared method across all three classes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2271139 and 8ac93f4.

📒 Files selected for processing (2)
  • data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7 hunks)
  • presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (2)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (1)
  • isImageMediaType (623-625)
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (1)
  • isImageMediaType (638-640)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (2)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (1)
  • isImageMediaType (154-156)
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt (1)
  • isImageMediaType (638-640)
🪛 detekt (1.23.8)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt

[warning] 445-445: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


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

(detekt.potential-bugs.ImplicitDefaultLocale)

🔇 Additional comments (9)
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt (2)

43-44: LGTM: Constructor properly wired with mimeTypes dependency.

The addition of mimeTypes as a parameter correctly supports the new thumbnail functionality and aligns with existing usage patterns in the data layer.


339-341: LGTM: Download icon replacement is straightforward.

The method provides a clean interface for the fragment layer to replace thumbnail images with a download indicator when needed.

data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt (7)

4-84: LGTM: Imports and field declarations are well-structured.

The imports and lazy initialization of the thread pool are appropriate. Using newCachedThreadPool with a custom thread factory ensures thumbnail generation scales efficiently without blocking the main decryption flow.


86-126: LGTM: Cache management methods are well-implemented.

The lazy initialization using computeIfAbsent ensures thread-safe cache creation, and the error handling appropriately logs failures while returning null to allow graceful degradation. The IllegalStateException now includes a descriptive message as requested in previous reviews.


441-453: LGTM: Timeout and cleanup logic is well-implemented.

The 5-second timeout prevents hanging on thumbnail generation, and the cleanup properly handles exceptions. The static analysis warning about swallowing the exception at line 445 is a false positive—the timeout is logged and the future is cancelled appropriately.

Note: If pipes are made nullable per earlier suggestions, line 442 and 451 need null-safe calls: thumbnailWriter?.let { closeQuietly(it) } and thumbnailReader?.let { closeQuietly(it) }.


539-541: LGTM: Thorough eligibility check for thumbnail generation.

The method correctly validates all preconditions before attempting thumbnail generation, with checks ordered efficiently from cheapest to most expensive.


543-571: LGTM: Explicit format filtering prevents SVG crash.

This method correctly excludes SVG (line 568) and other non-bitmap formats, directly addressing the crash reported in PR comments where BitmapFactory.decodeStream returns null for SVG files.


573-602: LGTM: Efficient batch thumbnail association with progress reporting.

The method efficiently associates cached thumbnails during directory listings, with appropriate early returns, progress reporting, and performance logging.


604-606: LGTM: Clear enablement check.

)
}
}
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.

@aiya000
Copy link

aiya000 commented Oct 20, 2025

@SailReal
Good morning☀

I noticed a bot comment above saying that the CLA license consent confirmation had not been obtained, so I have now completed the consent.
#533 (comment)

Also, I previously sent a quick message to (the email address above), but I haven't received a reply.
Is it okay to leave it as it is?

I look forward to the next commits and the completed PR 🙌✨

@SailReal
Copy link
Member

Also, I previously sent a quick message to (the email address above), but I haven't received a reply.

@aiya000 We responded to your email on the same day that we received it (09/08/2025)

@aiya000
Copy link

aiya000 commented Oct 20, 2025

Also, I previously sent a quick message to (the email address above), but I haven't received a reply.

@aiya000 We responded to your email on the same day that we received it (09/08/2025)

@SailReal
Aaah!!
That's true, I missed it...
I'm sorry. It's a little late, but I've replied...!!
thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants