Skip to content

Commit 2271139

Browse files
Merge pull request #8 from aiya000/fix/generate-thumbnails-for-larger-pictures
Resolve generating thumbnails of larger pictures
2 parents 24c9747 + 861f0be commit 2271139

File tree

8 files changed

+103
-9
lines changed

8 files changed

+103
-9
lines changed

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

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -427,18 +427,32 @@ abstract class CryptoImplDecorator(
427427
}
428428
}
429429
thumbnailWriter.flush()
430-
closeQuietly(thumbnailWriter)
431430
}
432431
} finally {
433432
encryptedTmpFile.delete()
434-
if (genThumbnail) {
435-
futureThumbnail.get()
436-
}
437433
progressAware.onProgress(Progress.completed(DownloadState.decryption(cryptoFile)))
438434
}
439435

436+
// Close thumbnail writer first, then wait for thumbnail generation to complete
437+
if (genThumbnail) {
438+
closeQuietly(thumbnailWriter)
439+
try {
440+
futureThumbnail.get(5, java.util.concurrent.TimeUnit.SECONDS) // Add timeout to prevent hanging
441+
} catch (e: java.util.concurrent.TimeoutException) {
442+
Timber.w("Thumbnail generation timed out for ${cryptoFile.name}")
443+
futureThumbnail.cancel(true)
444+
} catch (e: Exception) {
445+
Timber.w(e, "Error waiting for thumbnail generation for ${cryptoFile.name}")
446+
}
447+
}
440448
closeQuietly(thumbnailReader)
441449
} catch (e: IOException) {
450+
// Don't treat thumbnail-related pipe closed errors as fatal
451+
if (e.message?.contains("Pipe closed") == true && genThumbnail) {
452+
Timber.d("Pipe closed during thumbnail generation (expected): ${cryptoFile.name}")
453+
// The file was successfully decrypted, just the thumbnail failed
454+
return
455+
}
442456
throw FatalBackendException(e)
443457
}
444458
}
@@ -456,24 +470,68 @@ abstract class CryptoImplDecorator(
456470
try {
457471
val options = BitmapFactory.Options()
458472
val thumbnailBitmap: Bitmap?
459-
options.inSampleSize = 4 // pixel number reduced by a factor of 1/16
473+
474+
// Use aggressive sampling for memory efficiency with large images
475+
// Estimate file size and adjust sample size accordingly
476+
val fileSize = cryptoFile.size ?: 0L
477+
val fileSizeMB = fileSize / (1024 * 1024)
478+
479+
// Calculate sample size based on file size to prevent OOM
480+
var sampleSize = when {
481+
fileSizeMB > 50 -> 16 // 1/256 of original size for very large files
482+
fileSizeMB > 30 -> 12 // 1/144 of original size for large files
483+
fileSizeMB > 20 -> 8 // 1/64 of original size for medium-large files
484+
fileSizeMB > 10 -> 6 // 1/36 of original size for medium files
485+
else -> 4 // 1/16 of original size for smaller files
486+
}
487+
488+
options.inSampleSize = sampleSize
489+
options.inPreferredConfig = Bitmap.Config.RGB_565 // Use less memory than ARGB_8888
490+
options.inDither = false
491+
options.inPurgeable = true // Allow system to purge bitmap from memory if needed
492+
options.inInputShareable = true
493+
494+
Timber.d("Generating thumbnail for ${cryptoFile.name} (${fileSizeMB}MB) with sampleSize: $sampleSize")
495+
460496
val bitmap = BitmapFactory.decodeStream(thumbnailReader, null, options)
461497
if (bitmap == null) {
462498
closeQuietly(thumbnailReader)
499+
Timber.w("Failed to decode bitmap for thumbnail generation: ${cryptoFile.name}")
463500
return@submit
464501
}
465502

466503
val thumbnailWidth = 100
467504
val thumbnailHeight = 100
468505
thumbnailBitmap = ThumbnailUtils.extractThumbnail(bitmap, thumbnailWidth, thumbnailHeight)
506+
507+
// Clean up the original bitmap to free memory immediately
508+
if (bitmap != thumbnailBitmap) {
509+
bitmap.recycle()
510+
}
511+
469512
if (thumbnailBitmap != null) {
470513
storeThumbnail(diskCache, cacheKey, thumbnailBitmap)
514+
thumbnailBitmap.recycle() // Clean up thumbnail bitmap after storing
471515
}
472516
closeQuietly(thumbnailReader)
473517

474518
cryptoFile.thumbnail = diskCache[cacheKey]
519+
Timber.d("Successfully generated thumbnail for ${cryptoFile.name}")
520+
} catch (e: OutOfMemoryError) {
521+
closeQuietly(thumbnailReader)
522+
Timber.e(e, "OutOfMemoryError during thumbnail generation for large image: ${cryptoFile.name} (${(cryptoFile.size ?: 0L) / (1024 * 1024)}MB)")
523+
// Try to recover by forcing garbage collection
524+
System.gc()
525+
} catch (e: java.io.IOException) {
526+
closeQuietly(thumbnailReader)
527+
if (e.message?.contains("Pipe closed") == true) {
528+
Timber.d("Thumbnail generation stream closed (expected for large files): ${cryptoFile.name}")
529+
} else {
530+
Timber.w(e, "IOException during thumbnail generation for file: ${cryptoFile.name}")
531+
}
475532
} catch (e: Exception) {
476-
Timber.e(e, "Bitmap generation crashed")
533+
closeQuietly(thumbnailReader)
534+
Timber.e(e, "Bitmap generation crashed for file: ${cryptoFile.name}")
477535
}
478536
}
479537
}

presentation/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
android:allowBackup="false"
3333
android:icon="@mipmap/ic_launcher"
3434
android:label="@string/app_name"
35+
android:largeHeap="true"
3536
android:requestLegacyExternalStorage="true"
3637
android:supportsRtl="true"
3738
android:theme="@style/AppTheme"

presentation/src/main/java/org/cryptomator/presentation/model/CloudFileModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ class CloudFileModel(cloudFile: CloudFile, val icon: FileIcon) : CloudNodeModel<
1111

1212
val modified: Date? = cloudFile.modified
1313
val size: Long? = cloudFile.size
14-
var thumbnail : File? = if (cloudFile is CryptoFile) cloudFile.thumbnail else null
14+
val thumbnail : File?
15+
get() = if (toCloudNode() is CryptoFile) (toCloudNode() as CryptoFile).thumbnail else null
1516

1617
constructor(cloudFileRenamed: ResultRenamed<CloudFile>, icon: FileIcon) : this(cloudFileRenamed.value(), icon) {
1718
oldName = cloudFileRenamed.oldName

presentation/src/main/java/org/cryptomator/presentation/presenter/BrowseFilesPresenter.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ class BrowseFilesPresenter @Inject constructor( //
205205
.run(DefaultResultHandler())
206206
}
207207
setRefreshOnBackPressEnabled(enableRefreshOnBackpressSupplier.setInAction(false))
208+
209+
// Re-associate thumbnails for visible images when returning from image preview
210+
val images = view?.renderedCloudNodes()?.filterIsInstance<CloudFileModel>()?.filter { file ->
211+
isImageMediaType(file.name)
212+
} ?: return
213+
if (images.isNotEmpty()) {
214+
associateThumbnails(images.take(20)) // Increased from 10 to 20 for better coverage
215+
}
208216
}
209217

210218
fun onWindowFocusChanged(hasFocus: Boolean) {

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,17 @@ class ImagePreviewActivity : BaseActivity<ActivityImagePreviewBinding>(ActivityI
7373
toggleFullScreen()
7474
attachSystemUiVisibilityChangeListener()
7575
} catch (e: FatalBackendException) {
76-
showError(getString(R.string.error_generic))
76+
// Check if it's a memory issue with large images
77+
if (e.message?.contains("memory", ignoreCase = true) == true ||
78+
e.cause is OutOfMemoryError ||
79+
e.message?.contains("large", ignoreCase = true) == true) {
80+
showError(getString(R.string.error_image_too_large))
81+
} else {
82+
showError(getString(R.string.error_generic))
83+
}
84+
finish()
85+
} catch (e: OutOfMemoryError) {
86+
showError(getString(R.string.error_image_too_large))
7787
finish()
7888
}
7989
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,21 @@ class ImagePreviewFragment : Fragment() {
8383
binding.imageView.let { imageView ->
8484
imagePreviewFile?.let { imagePreviewFile ->
8585
imageView.orientation = SubsamplingScaleImageView.ORIENTATION_USE_EXIF
86-
imagePreviewFile.uri?.let { imageView.setImage(ImageSource.uri(it)) }
86+
// Configure SubsamplingScaleImageView for better memory handling of large images
87+
imageView.setMinimumTileDpi(160)
88+
imageView.setDebug(false)
89+
imageView.setDoubleTapZoomDpi(240)
90+
imageView.setDoubleTapZoomDuration(500)
91+
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE)
92+
// Remove the problematic line - SubsamplingScaleImageView will use default decoder
93+
imagePreviewFile.uri?.let {
94+
try {
95+
imageView.setImage(ImageSource.uri(it))
96+
} catch (e: OutOfMemoryError) {
97+
// Handle OOM gracefully
98+
presenter.onImagePreviewClicked() // This will show an error through the presenter
99+
}
100+
}
87101
}
88102
}
89103
}

presentation/src/main/res/values-ja-rJP/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<string name="share_with_label">暗号化</string>
55
<!-- # error messages -->
66
<string name="error_generic">エラーが発生しました</string>
7+
<string name="error_image_too_large">画像が大きすぎて表示できません。より小さい画像をお試しください。</string>
78
<string name="error_authentication_failed">認証に失敗しました</string>
89
<string name="error_authentication_failed_re_authenticate">認証に失敗しました。%1$s を使用してログインしてください</string>
910
<string name="error_no_network_connection">ネットワーク接続がありません</string>

presentation/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<!-- # error messages -->
1414
<string name="error_generic">An error occurred</string>
15+
<string name="error_image_too_large">Image is too large to display. Please try with a smaller image.</string>
1516
<string name="error_authentication_failed">Authentication failed</string>
1617
<string name="error_authentication_failed_re_authenticate">Authentication failed, please login using %1$s</string>
1718
<string name="error_no_network_connection">No network connection</string>

0 commit comments

Comments
 (0)