Skip to content

Conversation

@cameocoder
Copy link
Contributor

@cameocoder cameocoder commented Jun 10, 2025

Convert Downloads screen to Compose

Changes
DownloadsFragment is now just an empty shell used to display the Compose DownloadsScreen

These changes are built on top of update/dependency-coil3

Screenshot_20250610-103339

Summary by CodeRabbit

  • New Features

    • Introduced a redesigned downloads screen using a modern Compose-based interface, offering improved visuals and smoother interactions.
    • Added a confirmation dialog for deleting downloaded items.
  • Improvements

    • Enhanced download management with better state handling and asynchronous image loading.
    • Streamlined navigation and user interactions on the downloads screen.
  • Removals

    • Removed legacy downloads screen and related components based on the old View system.
  • Bug Fixes

    • General improvements to reliability and UI consistency for download-related actions.

rcrosbie added 7 commits June 5, 2025 18:01
Use coil to display the downloads thumbnail with error handling
…em to storage

This commit removes the local storage and loading of download thumbnails. Instead, thumbnails are now loaded directly from the server when displaying the downloads list.

The `DownloadUtils` no longer downloads and saves thumbnails locally.
The `DownloadsAdapter` now constructs the thumbnail URL using the `ApiClient` and loads it via Coil.
The `DownloadEntity` no longer stores the local thumbnail path.
Layout adjustments were made in `fragment_downloads.xml` and `download_item.xml` to accommodate this change and improve the display of download items.
A new dimension `movie_thumbnail_list_size` was added for consistent thumbnail sizing.

Note: Code to delete thumbnails is still in place to handle currently installed clients who may have existing downloads.
…oadsScreenRoot` Composable.

Removed unused fragment related code
- Move `DownloadsViewModel` to the `ui.screens.downloads` package.
- Remove unused drawable `ic_local_movies_white_64.xml`.
- Use `NotificationHelper` to build download completed notifications in `JellyfinDownloadService`.
- Add preview for DownloadsScreen

fun deleteDownload(mediaSourceId: String) {
viewModelScope.launch {
val downloadEntity: DownloadEntity = requireNotNull(downloadDao.get(mediaSourceId))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deleting the download should really be handled somewhere else, but moving the code here is no different than having it in MainActivity like is was before.

@nielsvanvelzen nielsvanvelzen marked this pull request as draft June 10, 2025 15:32
@nielsvanvelzen
Copy link
Member

Marked the PR as draft as it depends on another one.

@jellyfin-bot jellyfin-bot added the merge conflict Conflicts prevent merging label Jun 10, 2025
@Maxr1998
Copy link
Member

@cameocoder now that the base PR was merged, could you please rebase this PR and fix the conflicts? Thanks!

@jellyfin-bot jellyfin-bot removed the merge conflict Conflicts prevent merging label Jun 11, 2025
@nielsvanvelzen nielsvanvelzen marked this pull request as ready for review June 14, 2025 10:46
@nielsvanvelzen
Copy link
Member

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jun 14, 2025

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link

coderabbitai bot commented Jun 14, 2025

📝 Walkthrough

Walkthrough

This change migrates the downloads feature from a traditional Android View-based implementation to a Jetpack Compose-based UI. It removes legacy RecyclerView adapters, XML layouts, and ViewModel code, introducing new Compose screens, dialogs, and a refactored ViewModel. Gradle dependencies and library versions are updated to support Compose tooling and fragment integration.

Changes

File(s) / Group Change Summary
app/build.gradle.kts, gradle/libs.versions.toml Added Compose and fragment Compose dependencies, updated bundles for Compose and Coil.
app/src/main/java/org/jellyfin/mobile/app/AppModule.kt Updated DownloadsViewModel import and instantiation to new Compose-based location and constructor.
app/src/main/java/org/jellyfin/mobile/downloads/DownloadDiffCallback.kt,
DownloadsAdapter.kt,
DownloadsViewModel.kt
Deleted legacy RecyclerView diff callback, adapter, and ViewModel for downloads.
app/src/main/java/org/jellyfin/mobile/downloads/DownloadsFragment.kt Refactored fragment from View-based to Compose-based implementation, delegating UI to Compose screen.
app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt Replaced explicit notification construction with helper method; removed unused import.
app/src/main/java/org/jellyfin/mobile/events/ActivityEvent.kt Removed RemoveDownload event and related import.
app/src/main/java/org/jellyfin/mobile/events/ActivityEventHandler.kt Removed handling for RemoveDownload event and related import.
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DeleteDownloadConfirmationDialog.kt Added new Compose dialog for confirming download deletion, with preview.
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt Added full Compose UI for downloads: screen, list, item, loading, dialog, and previews.
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt Added new Compose-oriented ViewModel for downloads, with UI state, deletion, and open logic.
app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt Removed the removeDownload suspend function and related imports.
app/src/main/res/layout/activity_main.xml Removed design-time layout attribute from fragment container.
app/src/main/res/layout/download_item.xml,
fragment_downloads.xml
Deleted legacy XML layouts for download item and fragment UI.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DownloadsFragment
    participant DownloadsViewModel
    participant DownloadsScreen (Compose)
    participant DownloadDao
    participant ApiClient
    participant DownloadService

    User->>DownloadsFragment: Opens Downloads screen
    DownloadsFragment->>DownloadsScreen: Launches Compose UI with ViewModel
    DownloadsScreen->>DownloadsViewModel: Observes downloads state
    DownloadsViewModel->>DownloadDao: Fetch all downloads (Flow)
    DownloadDao-->>DownloadsViewModel: Emits list of downloads
    DownloadsViewModel-->>DownloadsScreen: Updates UI state with downloads
    DownloadsScreen-->>User: Displays list of downloads

    User->>DownloadsScreen: Clicks download item
    DownloadsScreen->>DownloadsViewModel: openDownload(mediaSourceId)
    DownloadsViewModel->>ApiClient: Prepares playback options
    DownloadsViewModel-->>DownloadsScreen: Emits event to open native player

    User->>DownloadsScreen: Long-presses download item
    DownloadsScreen->>DownloadsScreen: Shows DeleteDownloadConfirmationDialog
    User->>DownloadsScreen: Confirms deletion
    DownloadsScreen->>DownloadsViewModel: deleteDownload(mediaSourceId)
    DownloadsViewModel->>DownloadDao: Remove download entry
    DownloadsViewModel->>DownloadService: Remove media and subtitles
    DownloadsViewModel-->>DownloadsScreen: Updates UI state
Loading
✨ Finishing Touches
  • 📝 Generate Docstrings

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (8)
app/src/main/java/org/jellyfin/mobile/app/AppModule.kt (1)

80-81: Inject Context via get<Context>() rather than androidContext() for testability.

androidContext() hard-codes the Android app context, which makes it harder to swap in a mocked context during instrumented or JVM unit tests. Using the normal Koin lookup keeps the binding flexible:

-    viewModel { DownloadsViewModel(androidContext()) }
+    viewModel { DownloadsViewModel(get()) }

This still resolves to the same object in production but can be overridden in a test Koin module.

app/build.gradle.kts (1)

136-137: Add ui-tooling-preview to implementation for release-safe previews.

The common setup is:

implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)

ui.tooling.preview is stripped by R8/ProGuard, while ui.tooling stays in debug only. Keeping previews in the main artifact avoids “unresolved symbol” errors in release builds while not bloating APK size.

app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DeleteDownloadConfirmationDialog.kt (1)

30-37: Call onDismiss() after a positive confirmation to close the dialog.

Currently the dialog remains open until the caller’s state update propagates. For immediate UX feedback, invoke the dismiss callback right after onConfirm:

-                    onConfirm(mediaSourceId)
+                    onConfirm(mediaSourceId)
+                    onDismiss()

This makes the dialog disappear instantly and avoids a redundant recomposition.

app/src/main/java/org/jellyfin/mobile/downloads/DownloadsFragment.kt (2)

7-10: Importing the Composable ViewModel directly couples UI and DI layers.

If DownloadsScreenRoot is ever reused outside the fragment (e.g., in an Activity), consider exposing the ViewModel as a @Composable hiltViewModel()/koinViewModel() inside the composable instead of injecting it in the fragment. This keeps the fragment truly lightweight.


16-20: content {} is still marked @ExperimentalFragmentComposeApi.

Add an explicit opt-in to avoid build-time warnings:

@OptIn(ExperimentalFragmentComposeApi::class)
override fun onCreateView(...): View = content { ... }

This makes the experimental status explicit for future maintainers.

app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt (3)

75-85: Hard-coded strings & empty content description hurt i18n and accessibility

TopAppBar title uses "Downloads" literal and the back-arrow’s contentDescription is an empty string.

-title = { Text("Downloads") }
+title = { Text(stringResource(id = R.string.downloads_title)) }

-Icon(..., contentDescription = "")
+Icon(..., contentDescription = stringResource(id = R.string.back))

Please add the missing string resources; screen-reader users will thank you.


168-171: combinedClickable without indication removes default ripple

When wrapping a ListItem with combinedClickable, the default ripple is lost unless an indication = LocalIndication.current is provided.

.combinedClickable(
     onClick = { onSelectItem(...) },
     onLongClick = { onDeleteItem(mediaSourceId) },
+    indication = null,                  // < add proper indication or null intentionally
+    interactionSource = remember { MutableInteractionSource() },
 )

If visual feedback is desired, consider restoring ripple or migrate to Modifier.clickable {} + Modifier.pointerInput {} for long-press.


188-203: Fallback icon isn’t tint-aware

rememberVectorPainter(Icons.Default.LocalMovies) returns an untinted painter; on dark background it may end up invisible. Use painterResource + tint or set colorFilter:

error = rememberVectorPainter(Icons.Default.LocalMovies)
    .apply { /* tint with LocalContentColor.current */ }

Minor, but improves visual consistency.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2902752 and 8993548.

📒 Files selected for processing (17)
  • app/build.gradle.kts (2 hunks)
  • app/src/main/java/org/jellyfin/mobile/app/AppModule.kt (3 hunks)
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadDiffCallback.kt (0 hunks)
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadsAdapter.kt (0 hunks)
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadsFragment.kt (1 hunks)
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt (0 hunks)
  • app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt (1 hunks)
  • app/src/main/java/org/jellyfin/mobile/events/ActivityEvent.kt (0 hunks)
  • app/src/main/java/org/jellyfin/mobile/events/ActivityEventHandler.kt (0 hunks)
  • app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DeleteDownloadConfirmationDialog.kt (1 hunks)
  • app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt (1 hunks)
  • app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt (1 hunks)
  • app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt (0 hunks)
  • app/src/main/res/layout/activity_main.xml (0 hunks)
  • app/src/main/res/layout/download_item.xml (0 hunks)
  • app/src/main/res/layout/fragment_downloads.xml (0 hunks)
  • gradle/libs.versions.toml (4 hunks)
💤 Files with no reviewable changes (9)
  • app/src/main/java/org/jellyfin/mobile/events/ActivityEventHandler.kt
  • app/src/main/res/layout/activity_main.xml
  • app/src/main/java/org/jellyfin/mobile/events/ActivityEvent.kt
  • app/src/main/res/layout/fragment_downloads.xml
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadDiffCallback.kt
  • app/src/main/res/layout/download_item.xml
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadsViewModel.kt
  • app/src/main/java/org/jellyfin/mobile/utils/SystemUtils.kt
  • app/src/main/java/org/jellyfin/mobile/downloads/DownloadsAdapter.kt
🧰 Additional context used
🧬 Code Graph Analysis (1)
app/src/main/java/org/jellyfin/mobile/downloads/DownloadsFragment.kt (1)
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsScreen.kt (1)
  • DownloadsScreenRoot (47-71)
🔇 Additional comments (6)
app/src/main/java/org/jellyfin/mobile/app/AppModule.kt (1)

38-40: Correct package switch for the new ViewModel – looks good.

The import update to the new ui.screens.downloads namespace correctly removes the dependency on the deleted legacy class.

app/build.gradle.kts (1)

122-123: Confirm androidx.fragment:fragment-compose version compatibility.

fragment-compose is still in alpha; make sure the version pulled in by libs.androidx.fragment.compose matches your current Fragment/Compose Compiler versions to avoid ClassNotFoundException at runtime.

app/src/main/java/org/jellyfin/mobile/downloads/JellyfinDownloadService.kt (1)

80-85: Verify the parameter order for buildDownloadCompletedNotification.

The ExoPlayer API signatures changed between 1.x and 2.x lines. Check that the parameters you pass (context, icon, null, Util.fromUtf8Bytes(...)) map to (Context, Int, PendingIntent?, CharSequence?) in your dependency version; otherwise you’ll get a type-mismatch compilation error or a notification without content intent.

If the method expects contentIntent third and message fourth, swap the arguments:

-                    notificationHelper.buildDownloadCompletedNotification(
-                        context,
-                        R.drawable.ic_notification,
-                        null,
-                        Util.fromUtf8Bytes(download.request.data),
-                    )
+                    notificationHelper.buildDownloadCompletedNotification(
+                        context,
+                        R.drawable.ic_notification,
+                        Util.fromUtf8Bytes(download.request.data),
+                        null,
+                    )
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt (1)

61-71: Hard-coded stream indices may break playback

audioStreamIndex = 1 and subtitleStreamIndex = -1 assume a very specific media layout. If a download has only one audio track (index 0) or embedded subtitles, playback will fail.

Consider deriving the first available audio/subtitle index from mediaSource.audioStreams / subtitleStreams on the DownloadEntity instead of hard-coding.

gradle/libs.versions.toml (2)

107-117: compose-ui-tooling should never leak into release builds

compose-ui-tooling adds ~1 MB of methods and should be debugImplementation only. Ensure libs.compose.ui.tooling is referenced exclusively from the debug configuration (it is in build.gradle.kts currently).

No action needed if that constraint is already respected; just highlighting because the new library entry appears in the general libraries block.


165-166: Bundle now pulls in coil-compose for all consumers

Adding coil-compose to the coil bundle is convenient, but every consumer of bundles.coil now transitively depends on Compose, even non-UI modules. Double-check that this bundle isn’t used by pure JVM / instrumentation-only modules to avoid unnecessary dependencies.

Comment on lines +75 to +80
viewModelScope.launch {
val downloadEntity: DownloadEntity = requireNotNull(downloadDao.get(mediaSourceId))
val downloadDir = File(downloadEntity.mediaSource.localDirectoryUri)
downloadDao.delete(mediaSourceId)
downloadDir.deleteRecursively()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

requireNotNull will crash when the entity is missing

If the DB is already cleaned up (e.g. by another process) downloadDao.get(mediaSourceId) can return null, leading to an immediate crash.

Either:

val downloadEntity = downloadDao.get(mediaSourceId) ?: return@launch

or emit an error event so the UI can show feedback.

🤖 Prompt for AI Agents
In
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt
around lines 75 to 80, the use of requireNotNull on
downloadDao.get(mediaSourceId) can cause a crash if the entity is missing. To
fix this, replace requireNotNull with a safe call and handle the null case by
either returning early from the coroutine or emitting an error event for the UI.
For example, assign downloadEntity using a safe call with ?: return@launch to
avoid crashing when the entity is null.

Comment on lines +42 to +48
viewModelScope.launch { // this: CoroutineScope
downloadDao.getAllDownloads().flowOn(Dispatchers.IO).collect { downloadEntities: List<DownloadEntity> ->
val downloadModels = downloadEntities.map { it.asDownloadModel(applicationContext) }
_uiState.value = DownloadsUiState.ShowDownloads(downloadModels)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Collecting on Main thread may cause UI jank under large data sets

flowOn(Dispatchers.IO) moves the DAO query off-thread, but collect executes in the parent scope (viewModelScope, i.e. main). Mapping every entity to a DownloadModel for each emission could still become non-trivial when the list grows.

-viewModelScope.launch {
-    downloadDao.getAllDownloads()
-        .flowOn(Dispatchers.IO)
-        .collect { downloadEntities ->
-            _uiState.value = ...
-        }
-}
+viewModelScope.launch(Dispatchers.Default) {        // off-main for mapping
+    downloadDao.getAllDownloads()
+        .flowOn(Dispatchers.IO)                     // off-main for Room query
+        .collect { downloadEntities ->
+            _uiState.value = DownloadsUiState.ShowDownloads(
+                downloadEntities.map { it.asDownloadModel(applicationContext) }
+            )
+        }
+}

Using a background dispatcher for both the Room fetch and the mapping avoids blocking the main thread while still updating _uiState atomically.

📝 Committable suggestion

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

Suggested change
viewModelScope.launch { // this: CoroutineScope
downloadDao.getAllDownloads().flowOn(Dispatchers.IO).collect { downloadEntities: List<DownloadEntity> ->
val downloadModels = downloadEntities.map { it.asDownloadModel(applicationContext) }
_uiState.value = DownloadsUiState.ShowDownloads(downloadModels)
}
}
}
fun observeDownloads() {
- viewModelScope.launch { // this: CoroutineScope
- downloadDao.getAllDownloads().flowOn(Dispatchers.IO).collect { downloadEntities: List<DownloadEntity> ->
- val downloadModels = downloadEntities.map { it.asDownloadModel(applicationContext) }
- _uiState.value = DownloadsUiState.ShowDownloads(downloadModels)
- }
- }
+ viewModelScope.launch(Dispatchers.Default) { // off-main for mapping
+ downloadDao.getAllDownloads()
+ .flowOn(Dispatchers.IO) // off-main for Room query
+ .collect { downloadEntities ->
+ _uiState.value = DownloadsUiState.ShowDownloads(
+ downloadEntities.map { it.asDownloadModel(applicationContext) }
+ )
+ }
+ }
}
🤖 Prompt for AI Agents
In
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt
around lines 42 to 48, the flow collection and mapping are done on the main
thread, which can cause UI jank with large data sets. To fix this, move both the
Room fetch and the mapping operations to a background dispatcher by applying
flowOn(Dispatchers.Default) or another appropriate background dispatcher before
collect, ensuring the mapping does not block the main thread while still
updating _uiState atomically on the main thread.

Comment on lines +74 to +101
fun deleteDownload(mediaSourceId: String) {
viewModelScope.launch {
val downloadEntity: DownloadEntity = requireNotNull(downloadDao.get(mediaSourceId))
val downloadDir = File(downloadEntity.mediaSource.localDirectoryUri)
downloadDao.delete(mediaSourceId)
downloadDir.deleteRecursively()

val mediaSource = downloadEntity.mediaSource

val contentId = mediaSource.itemId.toString()
// Remove media file
DownloadService.sendRemoveDownload(
applicationContext,
JellyfinDownloadService::class.java,
contentId,
false,
)

// Remove subtitles
mediaSource.externalSubtitleStreams.forEach {
DownloadService.sendRemoveDownload(
applicationContext,
JellyfinDownloadService::class.java,
"$contentId:${it.index}",
false,
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

File & DB deletion run on main thread – move to Dispatchers.IO

deleteDownload touches the DB, performs File.deleteRecursively(), and calls DownloadService.sendRemoveDownload(...). All of that currently executes on the main thread because the surrounding launch {} inherits the default dispatcher from viewModelScope.

-viewModelScope.launch {
+viewModelScope.launch {
+    withContext(Dispatchers.IO) {
         val downloadEntity = downloadDao.get(mediaSourceId) ?: return@withContext
         val downloadDir = File(downloadEntity.mediaSource.localDirectoryUri)
         downloadDao.delete(mediaSourceId)
         downloadDir.deleteRecursively()
     }
 
     // Media3 API calls can stay on main
     ...
 }

Off-loading heavy I/O keeps UI responsive and prevents ANRs, especially when deleting large folders.

🤖 Prompt for AI Agents
In
app/src/main/java/org/jellyfin/mobile/ui/screens/downloads/DownloadsViewModel.kt
between lines 74 and 101, the deleteDownload function performs database
operations, file deletion, and service calls on the main thread due to the
default dispatcher in viewModelScope.launch. To fix this, wrap the entire
coroutine block inside viewModelScope.launch(Dispatchers.IO) to offload these
heavy I/O tasks to the IO dispatcher, ensuring the UI remains responsive and
preventing potential ANRs.

@jellyfin-bot jellyfin-bot added the merge conflict Conflicts prevent merging label Jun 22, 2025
@nielsvanvelzen
Copy link
Member

Please fix the merge requests so we can proceed with reviews

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

Labels

merge conflict Conflicts prevent merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants