delete unreferenced icons periodically and download updates every 24 hours

This commit is contained in:
Hunter Kehoe 2022-08-28 14:52:35 -06:00
parent c7edb50ebc
commit f68bb5f379
7 changed files with 35 additions and 47 deletions

View file

@ -380,8 +380,14 @@ interface NotificationDao {
@Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''") @Query("SELECT * FROM notification WHERE deleted = 1 AND attachment_contentUri <> ''")
fun listDeletedWithAttachments(): List<Notification> fun listDeletedWithAttachments(): List<Notification>
@Query("SELECT * FROM notification WHERE deleted = 1 AND icon_contentUri <> ''") @Query("SELECT DISTINCT icon_contentUri FROM notification WHERE deleted != 1 AND icon_contentUri <> ''")
fun listDeletedWithIcons(): List<Notification> fun listActiveIconUris(): List<String>
@Query("SELECT DISTINCT icon_contentUri FROM notification WHERE deleted = 1 AND icon_contentUri <> ''")
fun listDeletedIconUris(): List<String>
@Query("UPDATE notification SET icon_contentUri = null WHERE icon_contentUri = :uri")
fun clearIconUri(uri: String)
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun add(notification: Notification) fun add(notification: Notification)

View file

@ -92,8 +92,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
return notificationDao.listDeletedWithAttachments() return notificationDao.listDeletedWithAttachments()
} }
fun getDeletedNotificationsWithIcons(): List<Notification> { fun getActiveIconUris(): Set<String> {
return notificationDao.listDeletedWithIcons() return notificationDao.listActiveIconUris().toSet()
}
fun getDeletedIconUris(): Set<String> {
return notificationDao.listDeletedIconUris().toSet()
}
fun clearIconUri(uri: String) {
notificationDao.clearIconUri(uri)
} }
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> { fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {

View file

@ -14,11 +14,12 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.stringToHash import io.heckel.ntfy.util.sha256
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.io.File import java.io.File
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class DownloadIconWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { class DownloadIconWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
@ -45,7 +46,8 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
icon = notification.icon ?: return Result.failure() icon = notification.icon ?: return Result.failure()
try { try {
val iconFile = createIconFile(icon) val iconFile = createIconFile(icon)
if (!iconFile.exists()) { val yesterdayTimestamp = Date().time - 1000*60*60*24 // now Unix timestamp - 24 hours
if (!iconFile.exists() || iconFile.lastModified() < yesterdayTimestamp) {
downloadIcon(iconFile) downloadIcon(iconFile)
} else { } else {
Log.d(TAG, "Loading icon from cache: ${icon.url}") Log.d(TAG, "Loading icon from cache: ${icon.url}")
@ -152,7 +154,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters)
if (!iconDir.exists() && !iconDir.mkdirs()) { if (!iconDir.exists() && !iconDir.mkdirs()) {
throw Exception("Cannot create cache directory for icons: $iconDir") throw Exception("Cannot create cache directory for icons: $iconDir")
} }
val hash = stringToHash(icon.url) val hash = icon.url.sha256()
return File(iconDir, hash) return File(iconDir, hash)
} }

View file

@ -50,11 +50,7 @@ class NotificationParser {
) )
} }
} else null } else null
val icon: Icon? = if (message.icon != null) { val icon: Icon? = if (message.icon != null) Icon(url = message.icon) else null
Icon(
url = message.icon
)
} else null
val notification = Notification( val notification = Notification(
id = message.id, id = message.id,
subscriptionId = subscriptionId, subscriptionId = subscriptionId,

View file

@ -471,9 +471,8 @@ fun copyToClipboard(context: Context, notification: Notification) {
.show() .show()
} }
fun stringToHash(s: String): String { fun String.sha256(): String {
val bytes = s.toByteArray();
val md = MessageDigest.getInstance("SHA-256") val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(bytes) val digest = md.digest(this.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) } return digest.fold("") { str, it -> str + "%02x".format(it) }
} }

View file

@ -33,7 +33,6 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
deleteExpiredIcons() // Before notifications, so we will also catch manually deleted notifications deleteExpiredIcons() // Before notifications, so we will also catch manually deleted notifications
deleteExpiredAttachments() // Before notifications, so we will also catch manually deleted notifications deleteExpiredAttachments() // Before notifications, so we will also catch manually deleted notifications
deleteExpiredNotifications() deleteExpiredNotifications()
cleanIconCache()
return@withContext Result.success() return@withContext Result.success()
} }
} }
@ -68,40 +67,19 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx
Log.d(TAG, "Deleting icons for deleted notifications") Log.d(TAG, "Deleting icons for deleted notifications")
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
val repository = Repository.getInstance(applicationContext) val repository = Repository.getInstance(applicationContext)
val notifications = repository.getDeletedNotificationsWithIcons() val activeIconUris = repository.getActiveIconUris()
notifications.forEach { notification -> val expiredIconUris = repository.getDeletedIconUris()
val urisToDelete = expiredIconUris.minus(activeIconUris)
urisToDelete.forEach { uri ->
try { try {
val icon = notification.icon ?: return val deleted = resolver.delete(Uri.parse(uri), null, null) > 0
val contentUri = Uri.parse(icon.contentUri ?: return)
Log.d(TAG, "Deleting icon for notification ${notification.id}: ${icon.contentUri} (${icon.url})")
val deleted = resolver.delete(contentUri, null, null) > 0
if (!deleted) { if (!deleted) {
Log.w(TAG, "Unable to delete icon for notification ${notification.id}") Log.w(TAG, "Unable to delete icon at $uri")
} }
val newIcon = icon.copy(
contentUri = null, repository.clearIconUri(uri)
)
val newNotification = notification.copy(icon = newIcon)
repository.updateNotification(newNotification)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Failed to delete icon for notification: ${e.message}", e) Log.w(TAG, "Failed to delete icon: ${e.message}", e)
}
}
}
private fun cleanIconCache() {
Log.d(DeleteWorker.TAG, "Cleaning icons older than 24 hours from cache")
val iconDir = File(applicationContext.cacheDir, DownloadIconWorker.ICON_CACHE_DIR)
if (iconDir.exists()) {
for (f: File in iconDir.listFiles()) {
var lastModified = f.lastModified()
var today = Date()
var diffInHours = ((today.time - lastModified) / (1000 * 60 * 60))
if (diffInHours > 24) {
Log.d(DeleteWorker.TAG, "Deleting cached icon: ${f.name}")
f.delete()
}
} }
} }
} }

View file

@ -165,7 +165,6 @@
<string name="detail_item_download_info_download_failed">download failed</string> <string name="detail_item_download_info_download_failed">download failed</string>
<string name="detail_item_download_info_download_failed_expired">download failed, link expired</string> <string name="detail_item_download_info_download_failed_expired">download failed, link expired</string>
<string name="detail_item_download_info_download_failed_expires_x">download failed, link expires %1$s</string> <string name="detail_item_download_info_download_failed_expires_x">download failed, link expires %1$s</string>
<string name="detail_item_icon_download_failed">Could not download icon: %1$s</string>
<!-- Detail activity: Action bar --> <!-- Detail activity: Action bar -->
<string name="detail_menu_notifications_enabled">Notifications on</string> <string name="detail_menu_notifications_enabled">Notifications on</string>