Delete attachments for expired notifications regularly

This commit is contained in:
Philipp Heckel 2022-03-22 15:25:20 -04:00
parent a77d1a0f3a
commit c55693f9cf
6 changed files with 71 additions and 19 deletions

View file

@ -290,6 +290,9 @@ interface NotificationDao {
@Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId") // Includes deleted @Query("SELECT id FROM notification WHERE subscriptionId = :subscriptionId") // Includes deleted
fun listIds(subscriptionId: Long): List<String> fun listIds(subscriptionId: Long): List<String>
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
fun listDeletedWithAttachments(): List<Notification>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun add(notification: Notification) fun add(notification: Notification)

View file

@ -88,6 +88,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
return notificationDao.list() return notificationDao.list()
} }
fun getDeletedNotificationsWithAttachments(): List<Notification> {
return notificationDao.listDeletedWithAttachments()
}
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> { fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
return notificationDao.listFlow(subscriptionId).asLiveData() return notificationDao.listFlow(subscriptionId).asLiveData()
} }

View file

@ -355,7 +355,10 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
val resolver = context.applicationContext.contentResolver val resolver = context.applicationContext.contentResolver
val deleted = resolver.delete(contentUri, null, null) > 0 val deleted = resolver.delete(contentUri, null, null) > 0
if (!deleted) throw Exception("no rows deleted") 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) val newNotification = notification.copy(attachment = newAttachment)
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
repository.updateNotification(newNotification) repository.updateNotification(newNotification)

View file

@ -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. // (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! // 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 DELETE_WORKER_INTERVAL_MINUTES = 8 * 60L
const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L const val SERVICE_START_WORKER_INTERVAL_MINUTES = 3 * 60L
} }

View file

@ -1,14 +1,20 @@
package io.heckel.ntfy.work package io.heckel.ntfy.work
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.db.PROGRESS_DELETED
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.ui.DetailAdapter
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/**
* Deletes notifications marked for deletion and attachments for deleted notifications.
*/
class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
// IMPORTANT: // IMPORTANT:
// Every time the worker is changed, the periodic work has to be REPLACEd. // 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 { override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
Log.d(TAG, "Deleting expired notifications") deleteExpiredAttachments() // Before notifications, so we will also catch manually deleted notifications
val repository = Repository.getInstance(applicationContext) deleteExpiredNotifications()
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)
return@withContext Result.success() 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 { companion object {
const val VERSION = BuildConfig.VERSION_CODE const val VERSION = BuildConfig.VERSION_CODE
const val TAG = "NtfyDeleteWorker" const val TAG = "NtfyDeleteWorker"

View file

@ -1,5 +1,10 @@
Features: Features:
* Download attachments to cache folder (#181) * Download attachments to cache folder (#181)
* Regularly delete attachments for deleted notifications (#142)
Bugs: Bugs:
* IllegalStateException: Failed to build unique file (#177, thanks to @Fallenbagel for reporting) * 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