-
-
Notifications
You must be signed in to change notification settings - Fork 163
Thumbnail support #533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Thumbnail support #533
Conversation
WalkthroughAdds 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)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
Files ignored due to path filters (3)
presentation/src/main/res/values/arrays.xmlis excluded by!**/*.xmlpresentation/src/main/res/values/strings.xmlis excluded by!**/*.xmlpresentation/src/main/res/xml/preferences.xmlis 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 ofthumbnailproperty to handle optional thumbnail files forCloudFileModellooks good.data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoFile.kt (1)
16-16: Addition of nullablethumbnailproperty inCryptoFileclass is implemented correctly.presentation/src/main/java/org/cryptomator/presentation/ui/bottomsheet/FileSettingsBottomSheet.kt (1)
37-43: Handling of thumbnail display inFileSettingsBottomSheetusingBitmapFactoryand conditional UI updates is implemented correctly.util/src/main/java/org/cryptomator/util/file/LruFileCacheUtil.kt (1)
34-34: Addition ofLOCALoption to theCacheenum to support local caching of thumbnails is appropriate.presentation/src/main/java/org/cryptomator/presentation/ui/fragment/BrowseFilesFragment.kt (1)
29-29: Import ofSharedPreferencesHandlerinBrowseFilesFragmentfor accessing preferences is correctly added.data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplVaultFormatPre7.kt (1)
131-142: Integration of thumbnail caching inCryptoImplVaultFormatPre7is implemented correctly, handling cache retrieval and setting thumbnails appropriately.util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt (1)
164-171: Implementation ofgenerateThumbnails()inSharedPreferencesHandlerto 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 constantTHUMBNAIL_GENERATIONis used consistently throughout the code.
259-259: Ensure the functionality ofthumbnailGenerationChangeListeneris implemented as it is crucial for handling user preferences changes.
342-342: The constantTHUMBNAIL_GENERATIONis 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 methodisImageMediaTypeeffectively 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: MethodgetLruCacheForcorrectly abstracts the retrieval of the LRU cache based on the cloud type. This encapsulation aids maintainability.
98-109: TherenameFileInCachemethod 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 methodreadhandles the reading of files and the conditional generation of thumbnails. It uses aPipedOutputStreamandPipedInputStreamfor 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: ThestartThumbnailGeneratorThreadmethod 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 thatBitmapFactoryis 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 theImagePreviewFilesStoreis properly tested, especially its interaction with the UI.
| private fun setupThumbnailGeneration() { | ||
| val preference = findPreference(THUMBNAIL_GENERATION) as Preference? | ||
| // TODO ... | ||
| } |
There was a problem hiding this comment.
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.
| private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> | ||
| // TODO ... | ||
| true | ||
| } |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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.decodeFileinFileSettingsBottomSheet.ktandBrowseFilesAdapter.ktto 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
|
@JustFanta01 Not a review, but a general suggestion: Edit: my bad, this was the wrong dependency. |
|
Thank you so much for this contribution ❤️, will have a closer look to it on Monday!
@infeo can you please explain in detail why we should switch from DiskLruCache to Caffeine? |
|
@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. |
There was a problem hiding this 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
| private fun getOrCreateLruCache(key: LruFileCacheUtil.Cache, cacheSize: Int): DiskLruCache? { | ||
| return diskLruCache.computeIfAbsent(key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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) { |
| 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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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){ |
There was a problem hiding this comment.
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 {
| return buildString { | ||
| if (cloudFile.cloud?.id() != null) | ||
| this.append(cloudFile.cloud!!.id()) | ||
| else | ||
| this.append("c") // "common" | ||
| this.append("-") | ||
| this.append(cloudFile.path.hashCode()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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() && |
There was a problem hiding this comment.
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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove newline
| if(iv_file_image.drawable == null) | ||
| iv_file_image.setImageResource(cloudFileModel.icon.iconResource) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused layout
| private fun setupThumbnailGeneration() { | ||
| val preference = findPreference(THUMBNAIL_GENERATION) as Preference? | ||
| // TODO ... | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be removed
| private val thumbnailGenerationChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> | ||
| // TODO ... | ||
| true | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be removed
|
Also there is a bug if you use an svg-file, then In this case, the file can not be opened anymore. Please also test it with further other file types. |
…ng thumbnail from the DiskLruCache
Also using cloud.id to be able to distinguish between two files with same path but on different instances of the same cloud
a487f6d to
e05f206
Compare
…ally generate thumbnails for latest photos
There was a problem hiding this 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
@Parameterannotation on thelistparameter is noted.Consider adding a null check for the
cloudContentRepositoryparameter 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
executemethod is appropriately defined and follows good practices:
- It takes a
ProgressAware<FileTransferState>parameter for progress tracking.- It can throw a
BackendExceptionfor error handling.- It correctly delegates the work to the repository, adhering to the separation of concerns principle.
Consider adding a null check for the
progressAwareparameter 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
AssociateThumbnailsclass is a well-structured implementation of the thumbnail association use case. It aligns with the PR objectives and follows good software engineering practices:
- Clear separation of concerns
- Dependency injection
- Proper error handling
- 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_entriesarray 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_entriesandthumbnail_generation_valuesarrays 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:
- Clarify the status of the "per folder" option in comments or documentation.
- Ensure consistency between the arrays and the actual implementation, especially regarding the "PER_FOLDER" option.
- 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 methodassociateThumbnailsaddedThe new method
associateThumbnailsis 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 interfaceThe changes to
CloudContentRepository.ktare minimal and focused, adding support for thumbnail functionality as outlined in the PR objectives. The newassociateThumbnailsmethod 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:
- 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).
- Consider adding a method to retrieve thumbnails, which might be useful for UI components that display file listings with thumbnails.
- 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 methodassociateThumbnailsimplemented correctly.The new method is well-implemented and aligns with the PR objectives for thumbnail functionality. It correctly delegates the call to the
cryptoImplinstance and usesProgressAwarefor 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
associateThumbnailsmethod is well-integrated into the existingCryptoCloudContentRepositoryclass, following the established patterns and coding style.Key points:
- The new import and method are correctly placed and implemented.
- The changes are minimal and focused, reducing the risk of unintended side effects.
- The use of
ProgressAwareallows 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:
- Ensure that the
cryptoImplimplementations (e.g.,CryptoImplVaultFormat8,CryptoImplVaultFormat7, etc.) have consistent implementations of theassociateThumbnailsmethod.- Monitor the performance impact of thumbnail generation and association, especially for large folders or slow network connections.
- 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
associateThumbnailsmethod is well-implemented and follows the existing patterns in the class. However, there are a couple of suggestions for improvement:
- Add KDoc documentation to explain the purpose of the method and its parameters.
- 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
CryptoFileis 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:
- Error Handling: Add try-catch block to handle potential exceptions during cache deletion.
- Race Conditions: Consider potential race conditions where the cache entry might be accessed while being deleted.
- 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()andabort()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'swhenexpression 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 andTHUMBNAIL_GENERATIONconstant are seamlessly integrated into theSharedPreferencesHandlerclass. 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 keyThe addition of the
THUMBNAIL_GENERATIONconstant 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:
- Add a new string resource in your
strings.xmlfile:<string name="pref_key_thumbnail_generation" translatable="false">thumbnailGeneration</string>
- Update the constant in the
SettingsFragment:private const val THUMBNAIL_GENERATION = R.string.pref_key_thumbnail_generation
- 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 changesThe changes to the
SettingsFragmentimplement 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:
- The
setupThumbnailGenerationmethod is still missing and should be implemented to properly initialize the thumbnail generation preference.- The handling of the LRU cache and thumbnail generation settings in
useLruChangedListenercan be refactored for better organization and error handling.- Consider using a resource string for the
THUMBNAIL_GENERATIONconstant 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:
- Consider extracting the thumbnail generation logic into a separate class or helper methods to keep the
SettingsFragmentfocused on UI-related tasks.- Implement proper error handling for cases where preferences might not be found.
- Ensure that the state of the thumbnail generation preference is properly persisted and restored across app restarts.
- 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@Overrideannotations in KotlinThe
@Overrideannotation is not used in Kotlin when overriding methods; instead, theoverridekeyword 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
📒 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
ThumbnailsOptionenum 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 unimplementedPER_FOLDERoption 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_FOLDERoption.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 (
@Parameterand@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_valuesarray is well-structured and provides clear, descriptive values that correspond to the entries in thethumbnail_generation_entriesarray.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:
- Remove the "PER_FOLDER" option from both arrays until it's implemented.
- 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_valuesarray 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 FileTransferStateThe import of
FileTransferStateis appropriate and necessary for the newassociateThumbnailsmethod.data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoCloudContentRepository.kt (1)
16-16: LGTM: Import statement added correctly.The new import for
FileTransferStateis necessary for the newly addedassociateThumbnailsmethod 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_valuesTo 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
thumbnailGenerationpreference 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
FileTransferStateis necessary for the newassociateThumbnailsmethod 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_GENERATIONconstant 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
SharedPreferencesHandlerclass 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
renameFileInCacheis 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
decoratemethod? Specifically:
- What operations does it perform on the data?
- Are there any performance implications?
- 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.
decoratemethod is defined and utilized appropriately in related classes:
DataSource.ktUriBasedDataSource.ktCryptoImplDecorator.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 kotlinLength 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:
- Four options for thumbnail generation have been added: "Never", "Read Only", "Generate Per File", and "Generate Per Folder".
- A toast message for when LRU cache is disabled has been included.
- 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: EnsureRecyclerViewsupports fast scroll listener methodsVerify that
setOnFastScrollStateChangeListeneris available for yourRecyclerView. If you're using a customRecyclerViewor 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Add a TODO comment to remind developers about the unimplemented state.
- Use the
@Deprecatedannotation with a message explaining that it's not yet implemented. - 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|
|
||
| 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 | ||
| } |
There was a problem hiding this comment.
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:
- 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.
- 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.
- The implementation of
setupThumbnailGenerationis 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| import android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE | |
| import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE |
| override fun onFastScrollStart() { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| override fun onFastScrollStart() { | |
| } | |
| override fun onFastScrollStop() { | |
| thumbnailsForVisibleNodes() | |
| } | |
| } |
🧰 Tools
🪛 detekt
[warning] 95-96: This empty block of code can be removed.
(detekt.empty-blocks.EmptyFunctionBlock)
| val first = layoutManager.findFirstVisibleItemPosition() | ||
| val last = layoutManager.findLastVisibleItemPosition() | ||
| val visibleCloudNodes = cloudNodesAdapter.renderedCloudNodes().subList(first, last + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | |
| } | |
| } |
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
Outdated
Show resolved
Hide resolved
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
Outdated
Show resolved
Hide resolved
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt
Outdated
Show resolved
Hide resolved
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt
Outdated
Show resolved
Hide resolved
| @Override | ||
| override fun onProgress(progress: Progress<FileTransferState>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| @Override | |
| override fun onProgress(progress: Progress<FileTransferState>) { | |
| override fun onProgress(progress: Progress<FileTransferState>) { |
There was a problem hiding this 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 emptyonFastScrollStartmethodThe
onFastScrollStartmethod 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
readmethod for incorporating thumbnail generation are generally good. However, there's a potential issue with error handling:
- If an exception occurs during the file reading process, the
thumbnailWriterandthumbnailReadermight not be properly closed.- 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.ktsuccessfully implement thumbnail generation and caching functionality. The code is generally well-structured and efficient. However, there are several areas where improvements can be made:
- Error handling: Enhance exception handling in the
readmethod andstartThumbnailGeneratorThreadto provide more specific error messages and ensure proper resource cleanup.- Resource management: Ensure that all resources (especially
InputStreams and temporary files) are properly closed or deleted, even in error scenarios.- Consistency: Use explicit locales in string formatting to ensure consistent behavior across different devices.
- Logging: Improve logging to aid in debugging and monitoring, particularly for the
closeQuietlymethod.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 methodthumbnailsForVisibleNodes: 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
filterinstead offorEachfor 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 methodassociateThumbnails: 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
📒 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 functionalityThe 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 implementationThe new
onScrollListenerimplementation is well-designed. It callsthumbnailsForVisibleNodes()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 listenersThe new listeners
onFastScrollStateChangeListenerandonScrollListenerare correctly added to the RecyclerView in thesetupViewmethod. 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-implementedassociateThumbnailsmethod.The
associateThumbnailsmethod is well-structured and efficient:
- It properly checks if thumbnail generation is enabled before proceeding.
- The filtering process ensures only relevant files (images without thumbnails) are processed.
- The use of
measureTimeMillisfor performance logging is a good practice for monitoring and future optimizations.- 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
AssociateThumbnailsUseCaseandFileTransferStateare 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
associateThumbnailsUseCaseparameter to the constructor is correct and follows the existing pattern of dependency injection.
261-261: Modification togetCloudListmethod: 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 toinitblock: LGTM!The additions to unsubscribe
getDecryptedCloudForVaultUseCaseandassociateThumbnailsUseCasein theinitblock 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.ktThe modifications to implement thumbnail functionality in the
BrowseFilesPresenterclass 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:
- Proper dependency injection for the new use case.
- Correct implementation of user preference checks before performing thumbnail operations.
- Appropriate error handling and progress reporting in the new methods.
- 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
Show resolved
Hide resolved
| protected fun generateCacheKey(cryptoFile: CryptoFile): String { | ||
| return String.format("%s-%d", cryptoFile.cloudFile.cloud?.id() ?: "common", cryptoFile.path.hashCode()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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)
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
Show resolved
Hide resolved
data/src/main/java/org/cryptomator/data/cloud/crypto/CryptoImplDecorator.kt
Show resolved
Hide resolved
| 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) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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
_toastas 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
📒 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.
| <string name="thumbnail_generation_never">Never</string> | ||
| <string name="thumbnail_generation_readonly">Read Only</string> |
There was a problem hiding this comment.
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.
|
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. |
There was a problem hiding this 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 availabilityWhen 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 toThumbnailsOption.NEVER).
After a process death the user will therefore see the preference re-enabled while the cache is still disabled, leading to inconsistent behaviour insideBrowseFilesPresenter.Consider:
- Persisting a sane default (
ThumbnailsOption.NEVER) when disabling the cache.- 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_GENERATIONis 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
thumbnailsForVisibleNodesis called every time the view finishesassociateThumbnails().
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-flightdownloadFilesUseCasebefore starting a new one.
180-215: Method name is misleading – thumbnails are not generated here
downloadAndGenerateThumbnailstriggers only the download; the actual thumbnail creation is delegated elsewhere.
To avoid confusion for future maintainers (and incorrect assumptions in tests), either:
- Rename the method to
downloadVisibleImages(), or- Trigger thumbnail generation explicitly after the download finishes (e.g. invoke
associateThumbnailsUseCasefor 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
📒 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
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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 | |
| } |
presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt
Outdated
Show resolved
Hide resolved
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.
There was a problem hiding this 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
onFastScrollStartmethod 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
associateThumbnailsmethod inBrowseFilesPresenterto 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
📒 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
hideProgressandshowProgress).
presentation/src/main/java/org/cryptomator/presentation/ui/adapter/BrowseFilesAdapter.kt
Show resolved
Hide resolved
- 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
|
I would like to inform you ✨ https://github.com/aiya000/cryptomator-android The work I'm doing first is:
And I've submitted a PR to JustFanta01 for this: Anticipating that there might be no response from JustFanta01 to this PR (#533) and my above PR, I contacted you early. 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! Also, personally, I feel that thumbnail generation is always performed when accessing photos, even if they have already been generated. Taking these into account, I would be happy to discuss how to proceed with both of you 🙌 |
|
@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. |
|
Hi @aiya000 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 |
Thank you |
Thank you so much for replying! Yes, I will do that. |
…-pictures Resolve generating thumbnails of larger pictures
…supported image types
There was a problem hiding this 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.Timberpresentation/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)?.thumbnailpresentation/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
📒 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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| private var availableThumbnailsThreads = MAX_CONCURRENT_THUMBNAILS_THREADS | ||
| private val filesBeingDownloaded: MutableSet<CloudFileModel> = mutableSetOf() | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -50Length 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.ktLength 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.ktLength of output: 1122
🏁 Script executed:
#!/bin/bash
# Search for CloudModel implementation
cat presentation/src/main/java/org/cryptomator/presentation/model/CloudModel.ktLength of output: 683
🏁 Script executed:
#!/bin/bash
# Check if CloudFile is data class
rg -nB2 'class CloudFile|data class CloudFile' --type=kt domain/ | head -20Length 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.ktLength 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.ktLength 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 -5Length 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 -10Length 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 -20Length 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.ktLength of output: 50907
🏁 Script executed:
#!/bin/bash
# Search for cloudFileModelMapper to see how it creates models
find . -name '*cloudFileModelMapper*' -o -name '*CloudFileModelMapper*' 2>/dev/nullLength of output: 95
🏁 Script executed:
#!/bin/bash
# Look for mapper implementation
rg -n 'cloudFileModelMapper|CloudFileModelMapper' presentation/src/main/java/org/cryptomator/presentation/ --max-count=20Length 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.ktLength of output: 163
🏁 Script executed:
#!/bin/bash
# Check if CloudNode has proper equality
cat domain/src/main/java/org/cryptomator/domain/CloudNode.ktLength 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:
- Baton is not released:
availableThumbnailsThreads++is missing, leaving the semaphore permanently held at 0 and blocking all future thumbnail downloads. - In-flight entries are not cleared: Failed downloads remain in
filesBeingDownloaded, causing subsequent checks viacontains()to always skip those files. - 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.
There was a problem hiding this 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
genThumbnailis 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? = nullThen, 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.decodeFilecould return null for unsupported formats like SVG or corrupted files.One optional refinement:
BitmapFactory.decodeFilewithout options may load the entire bitmap into memory. If thumbnails are unexpectedly large, consider usingBitmapFactory.Optionswith bounds decoding to constrain memory usage.
154-156: ExtractisImageMediaTypeto eliminate duplication.This helper is duplicated identically in
CryptoImplDecorator.kt(lines 622–624) andBrowseFilesPresenter.kt(lines 637–639). Extract it to a shared utility class or extension function onMimeTypesto 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.,
MimeTypeUtilsor extendingMimeTypes) 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
📒 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
mimeTypesas 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
newCachedThreadPoolwith 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
computeIfAbsentensures 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) }andthumbnailReader?.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.decodeStreamreturns 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
|
@SailReal I noticed a bot comment above saying that the CLA license consent confirmation had not been obtained, so I have now completed the consent. Also, I previously sent a quick message to I look forward to the next commits and the completed PR 🙌✨ |
@aiya000 We responded to your email on the same day that we received it (09/08/2025) |
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.