Repository navigation
fix(attachment-preview): do not ask the content provider for the type during composition - #6857
Conversation
… during composition The preview called ContentResolver.getType for every file on the main thread in the body of the composable, so each recomposition (for example switching the file permission) made a binder call into the provider. A file shared from the Nextcloud Files app answers slowly, which froze the screen into an ANR. The type now comes from the file descriptions, which are read on the IO dispatcher. describeFiles no longer clears the descriptions before publishing the new ones, so the quality option, which depends on them, does not disappear while they are re-read. Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru> Assisted-by: Claude-Code:claude-sonnet-5-5
📝 Walkthrough
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
4f47a0a5-2365-496d-9e12-a1f58cf64993
📒 Files selected for processing (2)
app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewScreen.ktapp/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
The compression option sent with the files now depends on the file descriptions. Until they are read, the type of the files is unknown, so tapping send in that interval would turn compression off. Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru> Assisted-by: Claude-Code:claude-sonnet-5-5
rapterjet2004
left a comment
There was a problem hiding this comment.
Thanks, getting getType out of composition is the right fix. One concern with how it's done, though.
Send now waits for the full file copy. sendEnabled waits for every FileDescription, and describeFile calls FileUtils.getFileFromUri, which copies the whole file into the cache for content:// URIs (plus a video frame extraction and size estimate). For a large video shared from Nextcloud Files, Send stays disabled until the copy finishes, with no indication why. Before this change it could be sent immediately.
The type alone is a single ContentResolver.getType call. It is only slow because it runs on the main thread, so I'd resolve just the type off the main thread, separately from the full description. For example:
val context = LocalContext.current
val currentFileSet = currentFiles.toSet()
val hasCompressibleMedia by produceState(initialValue = false, currentFileSet) {
value = withContext(Dispatchers.IO) {
currentFileSet.any { isCompressible(FileUtils.resolveMimeType(context, it.toUri())) }
}
}(or a small mimeTypesByUri state in the ViewModel filled on Dispatchers.IO, so it survives rotation).
Send doesn't need to be gated either: UploadAndShareFilesWorker.compressMediaIfPossible already checks the type and only compresses images and videos, so passing compressImages as is stays correct. sendEnabled = currentFiles.isNotEmpty() can stay.
The putAll + retainAll change is a good one on its own, please keep it.
Minor (partly pre-existing): describeFiles launches a new job each time without cancelling the previous one. If an older job finishes last, its retainAll drops the description of a file that was just added. With clear() before, this only hid the preview; with the Send gate it would also leave Send disabled until the next change. Keeping a Job reference and cancelling it before launching a new one fixes it.
…scribed Send waited for every FileDescription, and describeFile copies content:// files into the cache, so Send stayed disabled until a large file was copied. Resolve only the MIME type, off the main thread, into a separate mimeTypesByUri state and enable Send as soon as there are files. The worker already compresses only images and videos, so compressImages is passed on as is. Cancel the previous describeFiles job before launching a new one, so a stale job finishing last cannot retainAll away a newly added description. Assisted-by: Claude-Code:claude-sonnet-5-5 Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt (1)
73-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest cancellation after the first job starts.
The test scheduler does not run the first
describeFilesjob before the second call cancels it. This test therefore passes without checking whether an in-progress, cancelled job can publish stale results. Start the first job, hold its describer, issue the second request, then release the first job and assert that the newer snapshot remains.
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
370d6305-bfde-4a84-b42e-61761579f010
📒 Files selected for processing (3)
app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewScreen.ktapp/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.ktapp/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
…led job cannot write describer and mimeTypeResolver are blocking, so a cancelled job still finished its copy and then ran putAll + retainAll with its stale snapshot, dropping the entry of a file added since. Publish the results through withContext(main): it throws if the job was cancelled and serialises the writes with cancel(), which also moves the snapshot-state mutation off the IO thread. The new test blocks the first describe call until the second call has finished. Assisted-by: Claude-Code:claude-sonnet-5-5 Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
|
@coderabbitai regarding the nitpick on |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd '^FileAttachmentPreview(ViewModelTest|ViewModel)\.kt$' app
ast-grep outline app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt --items all
ast-grep outline app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.kt --items all
sed -n '1,280p' app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt
rg -n -C 18 'fun describeFiles|withContext\(mainDispatcher\)|describeFilesJob' app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.ktLength of output: 9496
The If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! You are interacting with an AI system. |
|
Good catch, thanks. Pushed 3e6dcee and a9d9fd6:
|
rapterjet2004
left a comment
There was a problem hiding this comment.
Thanks, all points addressed. Resolving only the MIME type into its own state, not gating Send, and publishing both results through withContext(mainDispatcher) so a cancelled job that was still copying can't write anymore look good, and the test for the cancelled-but-still-running job is a nice touch.
Tiny optional nit: the test hooks (ioDispatcher, mainDispatcher, describer, mimeTypeResolver) could be constructor parameters with defaults instead of internal vars.
…ructor parameters The dispatchers, the describer and the MIME type resolver were internal vars that the test reassigned after construction. They are now constructor parameters with the production defaults; Dagger keeps a constructor that takes only the context. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
There was a problem hiding this comment.
🧹 Nitpick comments: 1
app/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt: 1
108-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace
Thread.sleepwith deterministic synchronization.The test uses
Thread.sleep(300)to wait for real IO work. On a slow CI machine, the seconddescribeFilesjob may not finish within 300 ms. The test then asserts too early and fails intermittently. Wait on aCountDownLatchthat the describer counts down for the second file. Also use a latch afterrelease.countDown()to confirm the first job finished.This is a test-quality improvement. The current logic is correct.
Also applies to: 116-116
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Organization UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
8a706c8f-233a-4208-8622-91f5ccb5bd17
📒 Files selected for processing: 2
app/src/main/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModel.ktapp/src/test/java/com/nextcloud/talk/attachmentpreview/FileAttachmentPreviewViewModelTest.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
|
Thanks! Done in 73d1c9f: |
Problem
FileAttachmentPreviewContentcomputedhasCompressibleMediain the body of the composable withFileUtils.resolveMimeType(context, uri)for every file, i.e.ContentResolver.getTypeon the main thread on every recomposition. For a file shared from the Nextcloud Files app this is a binder call into another process that answers slowly. Any recomposition of the preview (for example switching the file permission) blocked the main thread, and the screen froze into an ANR.Change
hasCompressibleMediais derived fromviewModel.descriptionsByUri(FileDescription.mimeType), whichdescribeFilesfills onDispatchers.IO. No second mechanism, no provider call during composition.compressImagesstarts frominitialCompressImages;onSendpassescompressImages && hasCompressibleMedia, so the value sent is the same as before.describeFilespublishes withputAll+retainAllinstead ofclear+putAll. Otherwise the quality option, which now depends on the descriptions, would vanish for a moment every time the descriptions are re-read (for example when the quality is toggled).How to test
./gradlew :app:compileGplayDebugKotlin :app:ktlintCheck :app:detektpass locally.ContentProviderand a real composition, and that needsunitTests.isIncludeAndroidResources = true, which breaks the other Robolectric tests of the module (manifest receivers). The main-thread provider calls were counted with such a test run locally, with a provider that sleeps 300 ms: 10 main-thread calls before the change, 0 after.🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)
The change was prepared with the help of Claude Code (Anthropic,
claude-sonnet-5-5). The author reviewed the code, built it and ran the static checks.🤖 Generated with Claude Code