delete unreferenced icons periodically and download updates every 24 hours
This commit is contained in:
parent
c7edb50ebc
commit
f68bb5f379
7 changed files with 35 additions and 47 deletions
|
@ -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)
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue