From 55270af5fb03fc31226cf54ee75aa3b07f5c9b4d Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:06:34 -0700 Subject: [PATCH] Implement HTTP Cache Checking --- .../org/cssnr/remotewallpaper/db/RemoteDao.kt | 13 ++++- .../remotewallpaper/ui/home/HomeFragment.kt | 52 +++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/cssnr/remotewallpaper/db/RemoteDao.kt b/app/src/main/java/org/cssnr/remotewallpaper/db/RemoteDao.kt index bb418bb..e589d8d 100644 --- a/app/src/main/java/org/cssnr/remotewallpaper/db/RemoteDao.kt +++ b/app/src/main/java/org/cssnr/remotewallpaper/db/RemoteDao.kt @@ -13,6 +13,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.Transaction import androidx.room.Upsert +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import java.util.concurrent.Executors @@ -68,10 +69,12 @@ data class Remote( //@PrimaryKey(autoGenerate = true) val id: Long = 0, @PrimaryKey val url: String, val active: Boolean = false, + val etag: String? = null, + val lastModified: String? = null, ) -@Database(entities = [Remote::class], version = 1, exportSchema = false) +@Database(entities = [Remote::class], version = 2, exportSchema = false) abstract class RemoteDatabase : RoomDatabase() { abstract fun remoteDao(): RemoteDao @@ -79,6 +82,13 @@ abstract class RemoteDatabase : RoomDatabase() { @Volatile private var instance: RemoteDatabase? = null + private val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Remote ADD COLUMN etag TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE Remote ADD COLUMN lastModified TEXT DEFAULT NULL") + } + } + private val defaultData: List = listOf( Remote("https://picsum.photos/4800/2400", active = true), Remote("https://picsum.photos/4800/2400?blur=10", active = false), @@ -103,6 +113,7 @@ abstract class RemoteDatabase : RoomDatabase() { } } }) + .addMigrations(MIGRATION_1_2) .build().also { instance = it } } } diff --git a/app/src/main/java/org/cssnr/remotewallpaper/ui/home/HomeFragment.kt b/app/src/main/java/org/cssnr/remotewallpaper/ui/home/HomeFragment.kt index 43596c0..6543754 100644 --- a/app/src/main/java/org/cssnr/remotewallpaper/ui/home/HomeFragment.kt +++ b/app/src/main/java/org/cssnr/remotewallpaper/ui/home/HomeFragment.kt @@ -32,6 +32,7 @@ import org.cssnr.remotewallpaper.R import org.cssnr.remotewallpaper.databinding.FragmentHomeBinding import org.cssnr.remotewallpaper.db.HistoryDatabase import org.cssnr.remotewallpaper.db.HistoryItem +import org.cssnr.remotewallpaper.db.Remote import org.cssnr.remotewallpaper.db.RemoteDatabase import java.io.File import java.io.FileOutputStream @@ -188,7 +189,7 @@ fun Context.showAddDialog() { if (url.isNotEmpty()) { CoroutineScope(Dispatchers.IO).launch { try { - downloadImage(url) + downloadImage(Remote(url = url)) withContext(Dispatchers.Main) { Toast.makeText(this@showAddDialog, "Done.", Toast.LENGTH_SHORT).show() dialog.dismiss() @@ -208,6 +209,11 @@ fun Context.showAddDialog() { dialog.show() } +sealed class DownloadResult { + data class Downloaded(val response: Response) : DownloadResult() + data object NotModified : DownloadResult() +} + // TODO: updateWallpaper is used globally to update the wallpaper and should be a package // The rest of the functions are only used by updateWallpaper and are internal to updateWallpaper suspend fun Context.updateWallpaper(): Boolean { @@ -220,10 +226,19 @@ suspend fun Context.updateWallpaper(): Boolean { Log.d("updateWallpaper", "remote: $remote") if (remote != null) { history.remote = remote.url - val response = withContext(Dispatchers.IO) { downloadImage(remote.url) } - history.status = response.code - history.url = response.request.url.toString() - Log.d("updateWallpaper", "response: $response") + val result = withContext(Dispatchers.IO) { downloadImage(remote) } + when (result) { + is DownloadResult.Downloaded -> { + history.status = result.response.code + history.url = result.response.request.url.toString() + Log.d("updateWallpaper", "response: ${result.response}") + } + is DownloadResult.NotModified -> { + history.status = 304 + history.url = remote.url + Log.i("updateWallpaper", "Image not modified, skipping wallpaper update") + } + } // TODO: Replace timestamp with history.timestamp val timestamp: String = ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME) @@ -246,19 +261,34 @@ suspend fun Context.updateWallpaper(): Boolean { } } -fun Context.downloadImage(url: String): Response { +suspend fun Context.downloadImage(remote: Remote): DownloadResult { val client = OkHttpClient.Builder() .followRedirects(true) .build() - val request = Request.Builder() - .url(url) - .build() + val requestBuilder = Request.Builder().url(remote.url) + remote.etag?.let { requestBuilder.header("If-None-Match", it) } + remote.lastModified?.let { requestBuilder.header("If-Modified-Since", it) } - val response = client.newCall(request).execute() + val response = client.newCall(requestBuilder.build()).execute() response.use { + if (it.code == 304) { + Log.d("downloadImage", "304 Not Modified for ${remote.url}") + return DownloadResult.NotModified + } if (!it.isSuccessful) throw Exception("Failed to download image: $it") + + val newEtag = it.header("ETag") + val newLastModified = it.header("Last-Modified") + if (newEtag != null || newLastModified != null) { + val dao = RemoteDatabase.getInstance(this).remoteDao() + withContext(Dispatchers.IO) { + dao.addOrUpdate(remote.copy(etag = newEtag, lastModified = newLastModified)) + } + Log.d("downloadImage", "Saved cache headers: etag=$newEtag, lastModified=$newLastModified") + } + val body = it.body val imageFile = File(filesDir, "wallpaper.img") @@ -270,7 +300,7 @@ fun Context.downloadImage(url: String): Response { setAutoCroppedWallpaper(imageFile) } - return response + return DownloadResult.Downloaded(response) } fun Context.setAutoCroppedWallpaper(imageFile: File) {