From c55693f9cf2c1fb267b1fce5334d692fec9175d2 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 22 Mar 2022 15:25:20 -0400 Subject: [PATCH] Delete attachments for expired notifications regularly --- .../main/java/io/heckel/ntfy/db/Database.kt | 3 + .../main/java/io/heckel/ntfy/db/Repository.kt | 4 ++ .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 5 +- .../java/io/heckel/ntfy/ui/MainActivity.kt | 2 +- .../java/io/heckel/ntfy/work/DeleteWorker.kt | 71 ++++++++++++++----- .../metadata/android/en-US/changelog/25.txt | 5 ++ 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index 272d597..ed3d550 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -290,6 +290,9 @@ interface NotificationDao { @Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId") // Includes deleted fun listIds(subscriptionId: Long): List + @Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''") + fun listDeletedWithAttachments(): List + @Insert(onConflict = OnConflictStrategy.IGNORE) fun add(notification: Notification) diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index d339ff8..c2b2b26 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -88,6 +88,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas return notificationDao.list() } + fun getDeletedNotificationsWithAttachments(): List { + return notificationDao.listDeletedWithAttachments() + } + fun getNotificationsLiveData(subscriptionId: Long): LiveData> { return notificationDao.listFlow(subscriptionId).asLiveData() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index e855ed1..4ff5e63 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -355,7 +355,10 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo val resolver = context.applicationContext.contentResolver val deleted = resolver.delete(contentUri, null, null) > 0 if (!deleted) throw Exception("no rows deleted") - val newAttachment = attachment.copy(progress = PROGRESS_DELETED) + val newAttachment = attachment.copy( + contentUri = null, + progress = PROGRESS_DELETED + ) val newNotification = notification.copy(attachment = newAttachment) GlobalScope.launch(Dispatchers.IO) { repository.updateNotification(newNotification) diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 8790e0d..cf6e554 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -663,7 +663,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc // (same as the JobScheduler API), but in practice 15 doesn't work. Using 16 here. // Thanks to varunon9 (https://gist.github.com/varunon9/f2beec0a743c96708eb0ef971a9ff9cd) for this! - const val POLL_WORKER_INTERVAL_MINUTES = 2 * 60L + const val POLL_WORKER_INTERVAL_MINUTES = 60L const val DELETE_WORKER_INTERVAL_MINUTES = 8 * 60L const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L } diff --git a/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt b/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt index a079c35..ee3772b 100644 --- a/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt @@ -1,14 +1,20 @@ package io.heckel.ntfy.work import android.content.Context +import android.net.Uri import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import io.heckel.ntfy.BuildConfig +import io.heckel.ntfy.db.PROGRESS_DELETED import io.heckel.ntfy.db.Repository +import io.heckel.ntfy.ui.DetailAdapter import io.heckel.ntfy.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +/** + * Deletes notifications marked for deletion and attachments for deleted notifications. + */ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { // IMPORTANT: // Every time the worker is changed, the periodic work has to be REPLACEd. @@ -20,27 +26,58 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx override suspend fun doWork(): Result { return withContext(Dispatchers.IO) { - Log.d(TAG, "Deleting expired notifications") - val repository = Repository.getInstance(applicationContext) - val deleteAfterSeconds = repository.getAutoDeleteSeconds() - if (deleteAfterSeconds == Repository.AUTO_DELETE_NEVER) { - Log.d(TAG, "Not deleting any notifications; global setting set to NEVER") - return@withContext Result.success() - } - - // Mark as deleted - val markDeletedOlderThanTimestamp = (System.currentTimeMillis()/1000) - deleteAfterSeconds - Log.d(TAG, "Marking notifications older than $markDeletedOlderThanTimestamp as deleted") - repository.markAsDeletedIfOlderThan(markDeletedOlderThanTimestamp) - - // Hard delete - val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS - Log.d(TAG, "Hard deleting notifications older than $markDeletedOlderThanTimestamp") - repository.removeNotificationsIfOlderThan(deleteOlderThanTimestamp) + deleteExpiredAttachments() // Before notifications, so we will also catch manually deleted notifications + deleteExpiredNotifications() return@withContext Result.success() } } + private fun deleteExpiredAttachments() { + Log.d(TAG, "Deleting attachments for deleted notifications") + val resolver = applicationContext.contentResolver + val repository = Repository.getInstance(applicationContext) + val notifications = repository.getDeletedNotificationsWithAttachments() + notifications.forEach { notification -> + try { + val attachment = notification.attachment ?: return + val contentUri = Uri.parse(attachment.contentUri ?: return) + Log.d(TAG, "Deleting attachment for notification ${notification.id}: ${attachment.contentUri} (${attachment.name})") + val deleted = resolver.delete(contentUri, null, null) > 0 + if (!deleted) { + Log.w(TAG, "Unable to delete attachment for notification ${notification.id}") + } + val newAttachment = attachment.copy( + contentUri = null, + progress = PROGRESS_DELETED + ) + val newNotification = notification.copy(attachment = newAttachment) + repository.updateNotification(newNotification) + } catch (e: Exception) { + Log.w(DetailAdapter.TAG, "Failed to delete attachment for notification: ${e.message}", e) + } + } + } + + private fun deleteExpiredNotifications() { + Log.d(TAG, "Deleting expired notifications") + val repository = Repository.getInstance(applicationContext) + val deleteAfterSeconds = repository.getAutoDeleteSeconds() + if (deleteAfterSeconds == Repository.AUTO_DELETE_NEVER) { + Log.d(TAG, "Not deleting any notifications; global setting set to NEVER") + return + } + + // Mark as deleted + val markDeletedOlderThanTimestamp = (System.currentTimeMillis()/1000) - deleteAfterSeconds + Log.d(TAG, "Marking notifications older than $markDeletedOlderThanTimestamp as deleted") + repository.markAsDeletedIfOlderThan(markDeletedOlderThanTimestamp) + + // Hard delete + val deleteOlderThanTimestamp = (System.currentTimeMillis()/1000) - HARD_DELETE_AFTER_SECONDS + Log.d(TAG, "Hard deleting notifications older than $markDeletedOlderThanTimestamp") + repository.removeNotificationsIfOlderThan(deleteOlderThanTimestamp) + } + companion object { const val VERSION = BuildConfig.VERSION_CODE const val TAG = "NtfyDeleteWorker" diff --git a/fastlane/metadata/android/en-US/changelog/25.txt b/fastlane/metadata/android/en-US/changelog/25.txt index 61f35d6..18a2d91 100644 --- a/fastlane/metadata/android/en-US/changelog/25.txt +++ b/fastlane/metadata/android/en-US/changelog/25.txt @@ -1,5 +1,10 @@ Features: * Download attachments to cache folder (#181) +* Regularly delete attachments for deleted notifications (#142) Bugs: * IllegalStateException: Failed to build unique file (#177, thanks to @Fallenbagel for reporting) + +Thanks: +* Many thanks to @cmeis, @Fallenbagel, @J117 and @rogeliodh for input on the new attachment logic, and for + testing the release