diff --git a/app/src/main/java/top/yeij/hearth/cache/CacheManager.kt b/app/src/main/java/top/yeij/hearth/cache/CacheManager.kt new file mode 100644 index 0000000..7f6589d --- /dev/null +++ b/app/src/main/java/top/yeij/hearth/cache/CacheManager.kt @@ -0,0 +1,18 @@ +package top.yeij.hearth.cache + +import java.io.File + +// 文件缓存:以 key 为文件名,内容以 UTF-8 纯文本落盘 +// File cache: uses key as filename, content stored as UTF-8 plain text +class CacheManager(private val baseDir: File) { + init { baseDir.mkdirs() } + + fun save(key: String, content: String) { + File(baseDir, key).writeText(content) + } + + fun load(key: String): String? { + val f = File(baseDir, key) + return if (f.exists()) f.readText() else null + } +} diff --git a/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt new file mode 100644 index 0000000..4a0b251 --- /dev/null +++ b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt @@ -0,0 +1,51 @@ +package top.yeij.hearth.webapp + +import com.google.gson.Gson +import top.yeij.hearth.cache.CacheManager + +// Web 应用清单条目 +// Web app manifest entry +data class WebApp(val id: String, val name: String, val icon: String, val url: String) + +// 网络抽象接口:Task 10 的 CardRepository 也会复用 +// HTTP abstraction reused by Task 10 CardRepository +interface HttpClient { + fun get(url: String): String? +} + +// 清单拉取仓库:拉取成功缓存 body 并解析;失败回退缓存;无缓存返回空列表 +// Manifest repository: on success cache + parse; on failure fall back to cache; no cache -> empty list +class WebAppRepository( + private val http: HttpClient, + private val cache: CacheManager, + private val manifestUrl: String, +) { + private val gson = Gson() + + fun fetchManifest(): List { + val body = http.get(manifestUrl) + if (body != null) { + cache.save(KEY, body) + return parse(body) + } + val cached = cache.load(KEY) ?: return emptyList() + return parse(cached) + } + + private fun parse(body: String): List { + val root = gson.fromJson(body, Map::class.java) + val apps = root["apps"] as? List<*> ?: return emptyList() + return apps.mapNotNull { m -> + val map = m as? Map<*, *> ?: return@mapNotNull null + val id = map["id"] as? String ?: return@mapNotNull null + val name = map["name"] as? String ?: return@mapNotNull null + val icon = map["icon"] as? String ?: return@mapNotNull null + val url = map["url"] as? String ?: return@mapNotNull null + WebApp(id, name, icon, url) + } + } + + companion object { + private const val KEY = "webapp-manifest" + } +} diff --git a/app/src/test/java/top/yeij/hearth/cache/CacheManagerTest.kt b/app/src/test/java/top/yeij/hearth/cache/CacheManagerTest.kt new file mode 100644 index 0000000..884f980 --- /dev/null +++ b/app/src/test/java/top/yeij/hearth/cache/CacheManagerTest.kt @@ -0,0 +1,25 @@ +package top.yeij.hearth.cache + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import java.io.File + +class CacheManagerTest { + @Test + fun saveThenLoad_roundTrips() { + val dir = File(System.getProperty("java.io.tmpdir"), "hearth-cache-test") + val cm = CacheManager(dir) + cm.save("manifest", "{\"version\":1}") + assertEquals("{\"version\":1}", cm.load("manifest")) + dir.deleteRecursively() + } + + @Test + fun load_missingKey_returnsNull() { + val dir = File(System.getProperty("java.io.tmpdir"), "hearth-cache-miss") + val cm = CacheManager(dir) + assertNull(cm.load("nonexistent")) + dir.deleteRecursively() + } +} diff --git a/app/src/test/java/top/yeij/hearth/webapp/WebAppRepositoryTest.kt b/app/src/test/java/top/yeij/hearth/webapp/WebAppRepositoryTest.kt new file mode 100644 index 0000000..ca70698 --- /dev/null +++ b/app/src/test/java/top/yeij/hearth/webapp/WebAppRepositoryTest.kt @@ -0,0 +1,44 @@ +package top.yeij.hearth.webapp + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import top.yeij.hearth.cache.CacheManager +import java.io.File + +class WebAppRepositoryTest { + @Test + fun fetchManifest_success_parsesApps() { + val http = object : HttpClient { + override fun get(url: String) = """{"version":1,"apps":[{"id":"a","name":"云音乐","icon":"x.svg","url":"http://x/a"}]}""" + } + val repo = WebAppRepository(http, CacheManager(File(System.getProperty("java.io.tmpdir"), "w1")), "http://fake") + val apps = repo.fetchManifest() + assertEquals(1, apps.size) + assertEquals("云音乐", apps[0].name) + } + + @Test + fun fetchManifest_failure_usesCache() { + val dir = File(System.getProperty("java.io.tmpdir"), "w2") + val cm = CacheManager(dir) + // 预写缓存(模拟之前拉取成功留下的缓存) + // Pre-seed the cache (simulate a previously successful fetch) + cm.save("webapp-manifest", """{"version":1,"apps":[{"id":"b","name":"缓存","icon":"x","url":"u"}]}""") + // 网络失败 + // Network failure + val http = object : HttpClient { override fun get(url: String): String? = null } + val repo = WebAppRepository(http, cm, "http://fake") + assertEquals("缓存", repo.fetchManifest()[0].name) + dir.deleteRecursively() + } + + @Test + fun fetchManifest_failure_withoutCache_returnsEmpty() { + val dir = File(System.getProperty("java.io.tmpdir"), "w3") + val http = object : HttpClient { override fun get(url: String): String? = null } + val repo = WebAppRepository(http, CacheManager(dir), "http://fake") + assertTrue(repo.fetchManifest().isEmpty()) + dir.deleteRecursively() + } +}