From 35e42e15e1788290d9feb72c9c18e81aed3662d5 Mon Sep 17 00:00:00 2001 From: Andead2 Date: Sun, 5 Oct 2025 14:37:52 +0200 Subject: [PATCH] Smart Delete feature --- app/src/main/AndroidManifest.xml | 3 + .../app/activities/BookListActivity.kt | 38 +++-- .../app/activities/SettingsActivity.kt | 135 ++++++++++++++++++ .../app/data/SmartDeleteSettings.kt | 6 + .../app/services/MyDownloadService.kt | 5 + .../app/userdata/UserDataManager.kt | 10 ++ .../app/utils/SmartDeleteManager.kt | 81 +++++++++++ 7 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/kaf/audiobookshelfwearos/app/activities/SettingsActivity.kt create mode 100644 app/src/main/java/kaf/audiobookshelfwearos/app/data/SmartDeleteSettings.kt create mode 100644 app/src/main/java/kaf/audiobookshelfwearos/app/utils/SmartDeleteManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b6798d6..f5d77a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,6 +52,9 @@ + + smartDeleteEnabled = enabled + userDataManager.smartDeleteEnabled = enabled + }, + label = { + Text("Smart Delete") + }, + toggleControl = { + Switch( + checked = smartDeleteEnabled, + onCheckedChange = null + ) + }, + modifier = Modifier.fillMaxWidth() + ) + } + + if (smartDeleteEnabled) { + item { + Text( + text = "Old audiobooks will be deleted once this amount is reached:", + style = MaterialTheme.typography.body2, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + item { + Text( + text = "Maximum Downloads", + style = MaterialTheme.typography.body1, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + item { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + // Minus button + Button( + onClick = { + if (maxDownloads > 1) { + maxDownloads-- + userDataManager.smartDeleteMaxDownloads = maxDownloads + } + }, + modifier = Modifier.size(40.dp), + enabled = maxDownloads > 1 + ) { + Text("-") + } + + // Current value + Text( + text = "$maxDownloads", + style = MaterialTheme.typography.title1, + modifier = Modifier.width(40.dp), + textAlign = TextAlign.Center + ) + + // Plus button + Button( + onClick = { + if (maxDownloads < 20) { + maxDownloads++ + userDataManager.smartDeleteMaxDownloads = maxDownloads + } + }, + modifier = Modifier.size(40.dp), + enabled = maxDownloads < 20 + ) { + Text("+") + } + } + } + } + } + } +} diff --git a/app/src/main/java/kaf/audiobookshelfwearos/app/data/SmartDeleteSettings.kt b/app/src/main/java/kaf/audiobookshelfwearos/app/data/SmartDeleteSettings.kt new file mode 100644 index 0000000..06693a6 --- /dev/null +++ b/app/src/main/java/kaf/audiobookshelfwearos/app/data/SmartDeleteSettings.kt @@ -0,0 +1,6 @@ +package kaf.audiobookshelfwearos.app.data + +data class SmartDeleteSettings( + val isEnabled: Boolean = true, + val maxDownloads: Int = 5 +) diff --git a/app/src/main/java/kaf/audiobookshelfwearos/app/services/MyDownloadService.kt b/app/src/main/java/kaf/audiobookshelfwearos/app/services/MyDownloadService.kt index 90a1c09..83c9239 100644 --- a/app/src/main/java/kaf/audiobookshelfwearos/app/services/MyDownloadService.kt +++ b/app/src/main/java/kaf/audiobookshelfwearos/app/services/MyDownloadService.kt @@ -27,6 +27,7 @@ import kaf.audiobookshelfwearos.app.data.DownloadState import kaf.audiobookshelfwearos.app.data.Track import kaf.audiobookshelfwearos.app.userdata.UserDataManager import kaf.audiobookshelfwearos.app.utils.DownloadProgressCalculator +import kaf.audiobookshelfwearos.app.utils.SmartDeleteManager import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -205,6 +206,10 @@ class MyDownloadService : DownloadService( Timber.i("Download completed: " + download.request.id) // Clear speed history for completed downloads DownloadProgressCalculator.clearSpeedHistory(download.request.id) + + // Trigger smart delete after download completion + val smartDeleteManager = SmartDeleteManager(context) + smartDeleteManager.triggerSmartDeleteAfterDownload() } Timber.d("onDownloadChanged ${download.state}") diff --git a/app/src/main/java/kaf/audiobookshelfwearos/app/userdata/UserDataManager.kt b/app/src/main/java/kaf/audiobookshelfwearos/app/userdata/UserDataManager.kt index 92b2a0b..ae54b26 100644 --- a/app/src/main/java/kaf/audiobookshelfwearos/app/userdata/UserDataManager.kt +++ b/app/src/main/java/kaf/audiobookshelfwearos/app/userdata/UserDataManager.kt @@ -17,6 +17,8 @@ class UserDataManager(context: Context) { private const val KEY_TOKEN = "token" private const val KEY_USERID = "userid" private const val KEY_OFFLINEMODE = "offlinemode" + private const val KEY_SMART_DELETE_ENABLED = "smart_delete_enabled" + private const val KEY_SMART_DELETE_MAX_DOWNLOADS = "smart_delete_max_downloads" } private val masterKey = MasterKey.Builder(context) @@ -63,6 +65,14 @@ class UserDataManager(context: Context) { get() = sharedPreferences.getBoolean(KEY_OFFLINEMODE, false) set(value) = sharedPreferences.edit().putBoolean(KEY_OFFLINEMODE, value).apply() + var smartDeleteEnabled: Boolean + get() = sharedPreferences.getBoolean(KEY_SMART_DELETE_ENABLED, true) + set(value) = sharedPreferences.edit().putBoolean(KEY_SMART_DELETE_ENABLED, value).apply() + + var smartDeleteMaxDownloads: Int + get() = sharedPreferences.getInt(KEY_SMART_DELETE_MAX_DOWNLOADS, 5) + set(value) = sharedPreferences.edit().putInt(KEY_SMART_DELETE_MAX_DOWNLOADS, value).apply() + fun clearUserData() { sharedPreferences.edit().clear().apply() } diff --git a/app/src/main/java/kaf/audiobookshelfwearos/app/utils/SmartDeleteManager.kt b/app/src/main/java/kaf/audiobookshelfwearos/app/utils/SmartDeleteManager.kt new file mode 100644 index 0000000..b226565 --- /dev/null +++ b/app/src/main/java/kaf/audiobookshelfwearos/app/utils/SmartDeleteManager.kt @@ -0,0 +1,81 @@ +package kaf.audiobookshelfwearos.app.utils + +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import android.content.Context +import android.widget.Toast +import androidx.media3.common.util.Log +import kaf.audiobookshelfwearos.app.MainApp +import kaf.audiobookshelfwearos.app.data.LibraryItem +import kaf.audiobookshelfwearos.app.services.MyDownloadService +import kaf.audiobookshelfwearos.app.userdata.UserDataManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import timber.log.Timber + +class SmartDeleteManager(private val context: Context) { + private val userDataManager = UserDataManager(context) + private val database = (context.applicationContext as MainApp).database + + fun triggerSmartDeleteAfterDownload() { + if (!userDataManager.smartDeleteEnabled) return + + CoroutineScope(Dispatchers.IO).launch { + delay(3_000) // 3 second delay + performSmartDelete() + } + } + + @OptIn(UnstableApi::class) + private suspend fun performSmartDelete() { + try { + val downloadManager = MyDownloadService.getDownloadManager(context) + val downloadedItems = database.libraryItemDao().getAllLibraryItems() + .filter { it.isDownloaded(context) } + + // Get download completion order by checking download index + val itemsWithDownloadInfo = downloadedItems.mapNotNull { item -> + val firstTrack = item.media.tracks.firstOrNull() + if (firstTrack != null) { + val download = downloadManager.downloadIndex.getDownload(firstTrack.contentUrl) + if (download != null) { + item to download.updateTimeMs + } else null + } else null + }.sortedBy { it.second } // Sort by download update time (oldest first) + + val maxDownloads = userDataManager.smartDeleteMaxDownloads + Timber.d("Smart delete max count: ${maxDownloads}") + val excessCount = itemsWithDownloadInfo.size - maxDownloads + Timber.d("Smart delete excess count: ${excessCount}") + + if (excessCount > 0) { + val itemsToDelete = itemsWithDownloadInfo.take(excessCount).map { it.first } + + for (item in itemsToDelete) { + // Remove downloads using the existing service + for (track in item.media.tracks) { + MyDownloadService.sendRemoveDownload(context, track) + } + + // Remove from database + database.libraryItemDao().deleteLibraryItem(item) + + CoroutineScope(Dispatchers.Main).launch { + Toast.makeText( + context, + "Removed ${item.title} to make space", + Toast.LENGTH_SHORT + ).show() + } + + Timber.d("Smart delete removed: ${item.title}") + } + } + } catch (e: Exception) { + Timber.e(e, "Error during smart delete") + } + } +}