mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat: cache AMSAT status while app is foreground
This commit is contained in:
1 parent
9a2a715d1f
commit
7924a6f69e
7 files changed
+209
-37
No files matched your search
@@ -17,7 +17,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat
|
package com.rtbishop.look4sat
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.Application.ActivityLifecycleCallbacks
|
||||||
|
import android.os.Bundle
|
||||||
import com.rtbishop.look4sat.core.data.injection.MainContainer
|
import com.rtbishop.look4sat.core.data.injection.MainContainer
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||||
@@ -35,12 +38,34 @@ class MainApplication : Application(), IContainerProvider {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
container = MainContainer(this)
|
container = MainContainer(this)
|
||||||
|
clearAmSatCacheOnAppBackground()
|
||||||
// trigger automatic update every 48 hours
|
// trigger automatic update every 48 hours
|
||||||
container.appScope.launch { checkAutoUpdate() }
|
container.appScope.launch { checkAutoUpdate() }
|
||||||
// load satellite data on every app start
|
// load satellite data on every app start
|
||||||
container.appScope.launch { container.satelliteRepo.initRepository() }
|
container.appScope.launch { container.satelliteRepo.initRepository() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearAmSatCacheOnAppBackground() {
|
||||||
|
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||||
|
private var startedActivityCount = 0
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
startedActivityCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStopped(activity: Activity) {
|
||||||
|
startedActivityCount = (startedActivityCount - 1).coerceAtLeast(0)
|
||||||
|
if (startedActivityCount == 0) container.amSatRepo.clearStatusCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
|
||||||
|
override fun onActivityResumed(activity: Activity) = Unit
|
||||||
|
override fun onActivityPaused(activity: Activity) = Unit
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||||
|
override fun onActivityDestroyed(activity: Activity) = Unit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun checkAutoUpdate(timeNow: Long = System.currentTimeMillis()) {
|
private suspend fun checkAutoUpdate(timeNow: Long = System.currentTimeMillis()) {
|
||||||
if (container.settingsRepo.otherSettings.value.stateOfAutoUpdate) {
|
if (container.settingsRepo.otherSettings.value.stateOfAutoUpdate) {
|
||||||
val timeDelta = timeNow - container.settingsRepo.databaseState.value.updateTimestamp
|
val timeDelta = timeNow - container.settingsRepo.databaseState.value.updateTimestamp
|
||||||
|
|||||||
+38
-13
@@ -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.repository.IAmSatRepository
|
||||||
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -31,26 +32,50 @@ private data class ApiReport(
|
|||||||
/** AMSAT status repository using RemoteSource (Clean Architecture: data layer handles HTTP). */
|
/** AMSAT status repository using RemoteSource (Clean Architecture: data layer handles HTTP). */
|
||||||
class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
|
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 {
|
private val isoUtcFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun fetchStatus(): SatStatusPage? = withContext(Dispatchers.IO) {
|
override fun getCachedStatus(): SatStatusPage? = statusCache
|
||||||
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)
|
override suspend fun fetchStatus(forceRefresh: Boolean): SatStatusPage? = withContext(Dispatchers.IO) {
|
||||||
val reports = parseReports(reportsJson)
|
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
|
||||||
|
|
||||||
if (names.isEmpty() && reports.isEmpty()) return@withContext null
|
val names = parseCatalog(catalogJson)
|
||||||
|
val reports = parseReports(reportsJson)
|
||||||
|
|
||||||
val statuses = buildStatuses(names, reports, nowSec)
|
if (names.isEmpty() && reports.isEmpty()) return@withContext null
|
||||||
val reportMap = reports.associate { it.id to toSatReport(it) }
|
|
||||||
SatStatusPage(System.currentTimeMillis(), statuses, reportMap)
|
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) {
|
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
|
||||||
|
}
|
||||||
+8
-2
@@ -6,8 +6,14 @@ import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
|||||||
|
|
||||||
/** AMSAT satellite status data source */
|
/** AMSAT satellite status data source */
|
||||||
interface IAmSatRepository {
|
interface IAmSatRepository {
|
||||||
/** Fetch and parse the AMSAT status page; null on failure */
|
/** Cached AMSAT status page for the current foreground session, if any. */
|
||||||
suspend fun fetchStatus(): SatStatusPage?
|
fun getCachedStatus(): SatStatusPage?
|
||||||
|
|
||||||
|
/** Fetch and parse the AMSAT status page; null on failure. */
|
||||||
|
suspend fun fetchStatus(forceRefresh: Boolean = false): SatStatusPage?
|
||||||
|
|
||||||
|
/** Clear the foreground-session status cache. */
|
||||||
|
fun clearStatusCache()
|
||||||
|
|
||||||
/** Submit a public AMSAT satellite status report. */
|
/** Submit a public AMSAT satellite status report. */
|
||||||
suspend fun submitReport(submission: AmSatReportSubmission): AmSatReportSubmitResult
|
suspend fun submitReport(submission: AmSatReportSubmission): AmSatReportSubmitResult
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -50,6 +51,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatDay
|
import com.rtbishop.look4sat.core.domain.model.SatDay
|
||||||
@@ -106,7 +110,22 @@ fun SatStatusDestination() {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||||
val viewModel: SatStatusViewModel = viewModel(factory = SatStatusViewModel.factory(container))
|
val viewModel: SatStatusViewModel = viewModel(factory = SatStatusViewModel.factory(container))
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
DisposableEffect(lifecycleOwner, viewModel) {
|
||||||
|
var skipInitialStart = true
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_START) {
|
||||||
|
if (skipInitialStart) {
|
||||||
|
skipInitialStart = false
|
||||||
|
} else {
|
||||||
|
viewModel.fetch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
SatStatusScreen(
|
SatStatusScreen(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
refresh = viewModel::refresh,
|
refresh = viewModel::refresh,
|
||||||
|
|||||||
+30
-20
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
|
|||||||
import com.rtbishop.look4sat.core.domain.model.AmSatReportSubmission
|
import com.rtbishop.look4sat.core.domain.model.AmSatReportSubmission
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatReport
|
import com.rtbishop.look4sat.core.domain.model.SatReport
|
||||||
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
@@ -51,28 +52,25 @@ class SatStatusViewModel(
|
|||||||
private val settingsRepo: ISettingsRepo
|
private val settingsRepo: ISettingsRepo
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SatStatusUiState(isLoading = true))
|
private val cachedStatusPage = amSatRepo.getCachedStatus()
|
||||||
|
private val _uiState = MutableStateFlow(cachedStatusPage?.toUiState() ?: SatStatusUiState(isLoading = true))
|
||||||
val uiState: StateFlow<SatStatusUiState> = _uiState
|
val uiState: StateFlow<SatStatusUiState> = _uiState
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetch()
|
if (cachedStatusPage == null) fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetch() {
|
fun fetch() {
|
||||||
|
amSatRepo.getCachedStatus()?.let { page ->
|
||||||
|
applyStatusPage(page)
|
||||||
|
return
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
try {
|
try {
|
||||||
val page = amSatRepo.fetchStatus()
|
val page = amSatRepo.fetchStatus()
|
||||||
if (page != null && page.statuses.isNotEmpty()) {
|
if (page != null && page.statuses.isNotEmpty()) {
|
||||||
_uiState.update {
|
applyStatusPage(page)
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
isRefreshing = false,
|
|
||||||
statuses = page.statuses,
|
|
||||||
reports = page.reports,
|
|
||||||
fetchedAtUtcMs = page.fetchedAtUtcMs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(isLoading = false, isRefreshing = false, error = "fetch_failed")
|
it.copy(isLoading = false, isRefreshing = false, error = "fetch_failed")
|
||||||
@@ -91,16 +89,9 @@ class SatStatusViewModel(
|
|||||||
_uiState.update { it.copy(isRefreshing = true, error = null) }
|
_uiState.update { it.copy(isRefreshing = true, error = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val page = amSatRepo.fetchStatus()
|
val page = amSatRepo.fetchStatus(forceRefresh = true)
|
||||||
if (page != null && page.statuses.isNotEmpty()) {
|
if (page != null && page.statuses.isNotEmpty()) {
|
||||||
_uiState.update {
|
applyStatusPage(page)
|
||||||
it.copy(
|
|
||||||
isRefreshing = false,
|
|
||||||
statuses = page.statuses,
|
|
||||||
reports = page.reports,
|
|
||||||
fetchedAtUtcMs = page.fetchedAtUtcMs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
_uiState.update { it.copy(isRefreshing = false, error = "fetch_failed") }
|
_uiState.update { it.copy(isRefreshing = false, error = "fetch_failed") }
|
||||||
}
|
}
|
||||||
@@ -110,6 +101,19 @@ class SatStatusViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun applyStatusPage(page: SatStatusPage) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isRefreshing = false,
|
||||||
|
statuses = page.statuses,
|
||||||
|
reports = page.reports,
|
||||||
|
fetchedAtUtcMs = page.fetchedAtUtcMs,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun toggleUploadPanel() {
|
fun toggleUploadPanel() {
|
||||||
val storedCallsign = settingsRepo.getAmSatCallsign()
|
val storedCallsign = settingsRepo.getAmSatCallsign()
|
||||||
val defaultGrid = defaultGridSquare()
|
val defaultGrid = defaultGridSquare()
|
||||||
@@ -275,6 +279,12 @@ class SatStatusViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SatStatusPage.toUiState() = SatStatusUiState(
|
||||||
|
statuses = statuses,
|
||||||
|
reports = reports,
|
||||||
|
fetchedAtUtcMs = fetchedAtUtcMs
|
||||||
|
)
|
||||||
|
|
||||||
private data class PreparedUpload(
|
private data class PreparedUpload(
|
||||||
val report: String,
|
val report: String,
|
||||||
val callsign: String,
|
val callsign: String,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionCode = "447"
|
appVersionCode = "448"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionName = "4.4.6-ba7opf.3"
|
appVersionName = "4.4.6-ba7opf.4"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
compileSdk = "37"
|
compileSdk = "37"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
|
|||||||
Reference in new issue
Block a user