mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 19:17:21 +00:00
feat: cache AMSAT status while app is foreground
This commit is contained in:
1 parent
9a2a715d1f
commit
7924a6f69e
7 files changed
+212
-40
No files matched your search
+41
-16
@@ -10,6 +10,7 @@ import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
||||
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -31,26 +32,50 @@ private data class ApiReport(
|
||||
/** AMSAT status repository using RemoteSource (Clean Architecture: data layer handles HTTP). */
|
||||
class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
|
||||
|
||||
private val statusCacheMutex = Mutex()
|
||||
|
||||
@Volatile
|
||||
private var statusCache: SatStatusPage? = null
|
||||
|
||||
@Volatile
|
||||
private var cacheGeneration = 0
|
||||
|
||||
private val isoUtcFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
override suspend fun fetchStatus(): SatStatusPage? = withContext(Dispatchers.IO) {
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
val catalogJson = remoteSource.getAmSatCatalog() ?: return@withContext null
|
||||
// 72h = 3 days; API hard cap is limit=500 regardless of what we send.
|
||||
// 500 records across ~100 catalog satellites ≈ ~1-5 reports/satellite/day — enough for 3 days.
|
||||
// Upgrade path: paginate or request AMSAT to raise the cap if catalog grows beyond ~200 sats.
|
||||
val reportsJson = remoteSource.getAmSatReports(hours = 72, limit = 500) ?: return@withContext null
|
||||
|
||||
val names = parseCatalog(catalogJson)
|
||||
val reports = parseReports(reportsJson)
|
||||
|
||||
if (names.isEmpty() && reports.isEmpty()) return@withContext null
|
||||
|
||||
val statuses = buildStatuses(names, reports, nowSec)
|
||||
val reportMap = reports.associate { it.id to toSatReport(it) }
|
||||
SatStatusPage(System.currentTimeMillis(), statuses, reportMap)
|
||||
override fun getCachedStatus(): SatStatusPage? = statusCache
|
||||
|
||||
override suspend fun fetchStatus(forceRefresh: Boolean): SatStatusPage? = withContext(Dispatchers.IO) {
|
||||
statusCacheMutex.lock()
|
||||
try {
|
||||
if (!forceRefresh) statusCache?.let { return@withContext it }
|
||||
val generation = cacheGeneration
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
val catalogJson = remoteSource.getAmSatCatalog() ?: return@withContext null
|
||||
// 72h = 3 days; API hard cap is limit=500 regardless of what we send.
|
||||
// 500 records across ~100 catalog satellites ≈ ~1-5 reports/satellite/day — enough for 3 days.
|
||||
// Upgrade path: paginate or request AMSAT to raise the cap if catalog grows beyond ~200 sats.
|
||||
val reportsJson = remoteSource.getAmSatReports(hours = 72, limit = 500) ?: return@withContext null
|
||||
|
||||
val names = parseCatalog(catalogJson)
|
||||
val reports = parseReports(reportsJson)
|
||||
|
||||
if (names.isEmpty() && reports.isEmpty()) return@withContext null
|
||||
|
||||
val statuses = buildStatuses(names, reports, nowSec)
|
||||
val reportMap = reports.associate { it.id to toSatReport(it) }
|
||||
SatStatusPage(System.currentTimeMillis(), statuses, reportMap).also { page ->
|
||||
if (generation == cacheGeneration) statusCache = page
|
||||
}
|
||||
} finally {
|
||||
statusCacheMutex.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearStatusCache() {
|
||||
statusCache = null
|
||||
cacheGeneration += 1
|
||||
}
|
||||
|
||||
override suspend fun submitReport(submission: AmSatReportSubmission): AmSatReportSubmitResult = withContext(Dispatchers.IO) {
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.rtbishop.look4sat.core.data.repository
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
||||
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
import java.io.InputStream
|
||||
|
||||
class AmSatRepositoryTest {
|
||||
|
||||
@Test
|
||||
fun fetchStatusReturnsSeededCacheUntilCacheIsCleared() = runTest {
|
||||
val remoteSource = FakeAmSatRemoteSource()
|
||||
val repository = AmSatRepository(remoteSource)
|
||||
val cachedPage = SatStatusPage(
|
||||
fetchedAtUtcMs = 123L,
|
||||
statuses = listOf(SatStatus(name = "AO-7", days = emptyList())),
|
||||
reports = emptyMap()
|
||||
)
|
||||
repository.seedStatusCache(cachedPage)
|
||||
|
||||
val firstPage = repository.fetchStatus()
|
||||
val secondPage = repository.fetchStatus()
|
||||
|
||||
assertSame(cachedPage, firstPage)
|
||||
assertSame(cachedPage, secondPage)
|
||||
assertSame(cachedPage, repository.getCachedStatus())
|
||||
assertEquals(0, remoteSource.catalogRequests)
|
||||
assertEquals(0, remoteSource.reportRequests)
|
||||
|
||||
repository.clearStatusCache()
|
||||
assertNull(repository.getCachedStatus())
|
||||
repository.fetchStatus()
|
||||
|
||||
assertEquals(1, remoteSource.catalogRequests)
|
||||
assertEquals(1, remoteSource.reportRequests)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun forceRefreshBypassesCachedPage() = runTest {
|
||||
val remoteSource = FakeAmSatRemoteSource()
|
||||
val repository = AmSatRepository(remoteSource)
|
||||
repository.seedStatusCache(
|
||||
SatStatusPage(
|
||||
fetchedAtUtcMs = 123L,
|
||||
statuses = listOf(SatStatus(name = "AO-7", days = emptyList())),
|
||||
reports = emptyMap()
|
||||
)
|
||||
)
|
||||
|
||||
repository.fetchStatus(forceRefresh = true)
|
||||
|
||||
assertEquals(1, remoteSource.catalogRequests)
|
||||
assertEquals(1, remoteSource.reportRequests)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AmSatRepository.seedStatusCache(page: SatStatusPage) {
|
||||
val cacheField = AmSatRepository::class.java.getDeclaredField("statusCache")
|
||||
cacheField.isAccessible = true
|
||||
cacheField.set(this, page)
|
||||
}
|
||||
|
||||
private class FakeAmSatRemoteSource : IRemoteSource {
|
||||
var catalogRequests = 0
|
||||
var reportRequests = 0
|
||||
|
||||
override suspend fun getFileStream(uri: String): InputStream? = null
|
||||
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = null
|
||||
|
||||
override suspend fun getAmSatCatalog(): String? {
|
||||
catalogRequests += 1
|
||||
return """{"data":[{"name":"AO-7"}]}"""
|
||||
}
|
||||
|
||||
override suspend fun getAmSatReports(hours: Int, limit: Int): String? {
|
||||
reportRequests += 1
|
||||
return """{"data":[]}"""
|
||||
}
|
||||
|
||||
override suspend fun submitAmSatReport(payloadJson: String): Pair<Int, String>? = null
|
||||
}
|
||||
Reference in new issue
Block a user