diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt index 2521f43..aa74157 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadIconWorker.kt @@ -14,7 +14,7 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.db.* import io.heckel.ntfy.util.Log -import io.heckel.ntfy.util.ensureSafeNewFile +import io.heckel.ntfy.util.stringToHash import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -44,7 +44,17 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure() icon = notification.icon ?: return Result.failure() try { - downloadIcon() + val iconFile = createIconFile(icon) + if (!iconFile.exists()) { + downloadIcon(iconFile) + } else { + Log.d(TAG, "Loading icon from cache: ${icon.url}") + val iconUri = createIconUri(iconFile) + this.uri = iconUri // Required for cleanup in onStopped() + save(icon.copy( + contentUri = iconUri.toString() + )) + } } catch (e: Exception) { failed(e) } @@ -56,7 +66,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) maybeDeleteFile() } - private fun downloadIcon() { + private fun downloadIcon(iconFile: File) { Log.d(TAG, "Downloading icon from ${icon.url}") try { @@ -74,7 +84,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) return } val resolver = applicationContext.contentResolver - val uri = createUri(notification) + val uri = createIconUri(iconFile) this.uri = uri // Required for cleanup in onStopped() Log.d(TAG, "Starting download to content URI: $uri") @@ -137,13 +147,17 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) return size > maxAutoDownloadSize } - private fun createUri(notification: Notification): Uri { + private fun createIconFile(icon: Icon): File { val iconDir = File(context.cacheDir, ICON_CACHE_DIR) if (!iconDir.exists() && !iconDir.mkdirs()) { throw Exception("Cannot create cache directory for icons: $iconDir") } - val file = ensureSafeNewFile(iconDir, notification.id) - return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file) + val hash = stringToHash(icon.url) + return File(iconDir, hash) + } + + private fun createIconUri(iconFile: File): Uri { + return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, iconFile) } companion object { @@ -152,7 +166,7 @@ class DownloadIconWorker(private val context: Context, params: WorkerParameters) const val MAX_ICON_DOWNLOAD_SIZE = 300000 private const val TAG = "NtfyIconDownload" - private const val ICON_CACHE_DIR = "icons" + const val ICON_CACHE_DIR = "icons" private const val BUFFER_SIZE = 8 * 1024 } } diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index a6a3197..59d1df3 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -38,6 +38,7 @@ import okhttp3.RequestBody import okio.BufferedSink import okio.source import java.io.* +import java.security.MessageDigest import java.security.SecureRandom import java.text.DateFormat import java.text.StringCharacterIterator @@ -469,3 +470,10 @@ fun copyToClipboard(context: Context, notification: Notification) { .makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG) .show() } + +fun stringToHash(s: String): String { + val bytes = s.toByteArray(); + val md = MessageDigest.getInstance("SHA-256") + val digest = md.digest(bytes) + return digest.fold("") { str, it -> str + "%02x".format(it) } +} \ No newline at end of file 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 b5706d7..ddeee83 100644 --- a/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/work/DeleteWorker.kt @@ -7,11 +7,14 @@ import androidx.work.WorkerParameters import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.db.ATTACHMENT_PROGRESS_DELETED import io.heckel.ntfy.db.Repository +import io.heckel.ntfy.msg.DownloadIconWorker import io.heckel.ntfy.ui.DetailAdapter import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.topicShortUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.io.File +import java.util.* /** * Deletes notifications marked for deletion and attachments for deleted notifications. @@ -30,6 +33,7 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx deleteExpiredIcons() // Before notifications, so we will also catch manually deleted notifications deleteExpiredAttachments() // Before notifications, so we will also catch manually deleted notifications deleteExpiredNotifications() + cleanIconCache() return@withContext Result.success() } } @@ -85,6 +89,23 @@ class DeleteWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx } } + 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() + } + } + } + } + private suspend fun deleteExpiredNotifications() { Log.d(TAG, "Deleting expired notifications") val repository = Repository.getInstance(applicationContext)