Skip to content

Commit

Permalink
Merge pull request #107 from CrisisCleanup/cache-form-data
Browse files Browse the repository at this point in the history
Cache form data
  • Loading branch information
hueachilles committed Mar 18, 2024
2 parents 001f530 + 0ef458d commit 12b50ff
Show file tree
Hide file tree
Showing 39 changed files with 5,204 additions and 117 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {

android {
defaultConfig {
val buildVersion = 190
val buildVersion = 192
applicationId = "com.crisiscleanup"
versionCode = buildVersion
versionName = "0.9.${buildVersion - 168}"
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/java/com/crisiscleanup/ui/CrisisCleanupApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.crisiscleanup.AuthState
import com.crisiscleanup.MainActivityViewModel
import com.crisiscleanup.MainActivityViewState
import com.crisiscleanup.core.common.NetworkMonitor
import com.crisiscleanup.core.designsystem.LayoutProvider
import com.crisiscleanup.core.designsystem.LocalAppTranslator
Expand Down Expand Up @@ -202,9 +203,16 @@ private fun BoxScope.LoadedContent(
}
BusyIndicatorFloatingTopCenter(isFetching)
} else {
val mainViewState by viewModel.viewState.collectAsStateWithLifecycle()
val hideOnboarding =
(mainViewState as? MainActivityViewState.Success)?.userData?.shouldHideOnboarding
?: true
val isOnboarding = !hideOnboarding

NavigableContent(
snackbarHostState,
appState,
isOnboarding,
) { openAuthentication = true }

if (
Expand Down Expand Up @@ -306,6 +314,7 @@ private fun AcceptTermsContent(
private fun NavigableContent(
snackbarHostState: SnackbarHostState,
appState: CrisisCleanupAppState,
isOnboarding: Boolean,
openAuthentication: () -> Unit,
) {
val showNavigation = appState.isTopLevelRoute
Expand Down Expand Up @@ -385,7 +394,7 @@ private fun NavigableContent(
onBack = appState::onBack,
openAuthentication = openAuthentication,
modifier = Modifier.weight(1f),
startDestination = appState.lastTopLevelRoute(),
startDestination = appState.lastTopLevelRoute(isOnboarding),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,14 @@ class CrisisCleanupAppState(
MENU,
)

private var priorTopLevelDestination: TopLevelDestination = MENU
private var priorTopLevelDestination: TopLevelDestination? = null

@Composable
fun lastTopLevelRoute(): String {
fun lastTopLevelRoute(isOnboarding: Boolean): String {
if (isOnboarding && priorTopLevelDestination == null) {
return MENU_ROUTE
}

return when (priorTopLevelDestination) {
DASHBOARD -> DASHBOARD_ROUTE
TEAM -> TEAM_ROUTE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import android.content.Context
import com.crisiscleanup.core.common.log.AppLogger
import com.crisiscleanup.core.common.log.CrisisCleanupLoggers
import com.crisiscleanup.core.common.log.Logger
import com.crisiscleanup.core.data.model.IncidentCacheDataPageRequest
import com.crisiscleanup.core.data.model.IncidentWorksitesPageRequest
import com.crisiscleanup.core.data.model.IncidentWorksitesSecondaryDataPageRequest
import com.crisiscleanup.core.network.CrisisCleanupNetworkDataSource
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.coroutineScope
Expand Down Expand Up @@ -37,6 +39,25 @@ interface WorksitesNetworkDataCache {
incidentId: Long,
pageIndex: Int,
)

fun loadWorksitesSecondaryData(
incidentId: Long,
pageIndex: Int,
expectedCount: Int,
): IncidentWorksitesSecondaryDataPageRequest?

suspend fun saveWorksitesSecondaryData(
incidentId: Long,
pageCount: Int,
pageIndex: Int,
expectedCount: Int,
updatedAfter: Instant?,
)

suspend fun deleteWorksitesSecondaryData(
incidentId: Long,
pageIndex: Int,
)
}

class WorksitesNetworkDataFileCache @Inject constructor(
Expand All @@ -48,16 +69,16 @@ class WorksitesNetworkDataFileCache @Inject constructor(
"incident-$incidentId-worksites-short-$page.json"

@OptIn(ExperimentalSerializationApi::class)
override fun loadWorksitesShort(
private inline fun <reified T : IncidentCacheDataPageRequest> loadCacheData(
cacheFileName: String,
incidentId: Long,
pageIndex: Int,
expectedCount: Int,
): IncidentWorksitesPageRequest? {
val cacheFileName = shortWorksitesFileName(incidentId, pageIndex)
): T? {
val cacheFile = File(context.cacheDir, cacheFileName)
if (cacheFile.exists()) {
cacheFile.inputStream().use {
val cachedData: IncidentWorksitesPageRequest = Json.decodeFromStream(it)
val cachedData: T = Json.decodeFromStream(it)
if (cachedData.incidentId == incidentId &&
cachedData.page == pageIndex &&
cachedData.totalCount == expectedCount &&
Expand All @@ -71,6 +92,17 @@ class WorksitesNetworkDataFileCache @Inject constructor(
return null
}

override fun loadWorksitesShort(
incidentId: Long,
pageIndex: Int,
expectedCount: Int,
) = loadCacheData<IncidentWorksitesPageRequest>(
shortWorksitesFileName(incidentId, pageIndex),
incidentId,
pageIndex,
expectedCount,
)

@OptIn(ExperimentalSerializationApi::class)
override suspend fun saveWorksitesShort(
incidentId: Long,
Expand All @@ -79,8 +111,6 @@ class WorksitesNetworkDataFileCache @Inject constructor(
expectedCount: Int,
updatedAfter: Instant?,
) = coroutineScope {
val cacheFileName = shortWorksitesFileName(incidentId, pageIndex)

try {
loadWorksitesShort(incidentId, pageIndex, expectedCount)?.let {
return@coroutineScope
Expand All @@ -106,6 +136,7 @@ class WorksitesNetworkDataFileCache @Inject constructor(
worksites,
)

val cacheFileName = shortWorksitesFileName(incidentId, pageIndex)
val cacheFile = File(context.cacheDir, cacheFileName)
cacheFile.outputStream().use {
Json.encodeToStream(dataCache, it)
Expand All @@ -115,8 +146,12 @@ class WorksitesNetworkDataFileCache @Inject constructor(
override suspend fun deleteWorksitesShort(
incidentId: Long,
pageIndex: Int,
) = coroutineScope {
) {
val cacheFileName = shortWorksitesFileName(incidentId, pageIndex)
deleteCacheFile(cacheFileName)
}

private suspend fun deleteCacheFile(cacheFileName: String) = coroutineScope {
try {
val cacheFile = File(context.cacheDir, cacheFileName)
if (cacheFile.exists()) {
Expand All @@ -126,4 +161,66 @@ class WorksitesNetworkDataFileCache @Inject constructor(
logger.logDebug("Error deleting cache file $cacheFileName. ${e.message}")
}
}

private fun secondaryDataFileName(incidentId: Long, page: Int) =
"incident-$incidentId-worksites-secondary-data-$page.json"

override fun loadWorksitesSecondaryData(
incidentId: Long,
pageIndex: Int,
expectedCount: Int,
) = loadCacheData<IncidentWorksitesSecondaryDataPageRequest>(
secondaryDataFileName(incidentId, pageIndex),
incidentId,
pageIndex,
expectedCount,
)

@OptIn(ExperimentalSerializationApi::class)
override suspend fun saveWorksitesSecondaryData(
incidentId: Long,
pageCount: Int,
pageIndex: Int,
expectedCount: Int,
updatedAfter: Instant?,
) = coroutineScope {
try {
loadWorksitesSecondaryData(incidentId, pageIndex, expectedCount)?.let {
return@coroutineScope
}
} catch (e: Exception) {
logger.logDebug("Error reading cache file ${e.message}")
}

val requestTime = Clock.System.now()
val worksites = networkDataSource.getWorksitesFlagsFormDataPage(
incidentId,
pageCount,
pageIndex + 1,
updatedAtAfter = updatedAfter,
)

val dataCache = IncidentWorksitesSecondaryDataPageRequest(
incidentId,
requestTime,
pageIndex,
pageIndex * pageCount,
expectedCount,
worksites,
)

val cacheFileName = secondaryDataFileName(incidentId, pageIndex)
val cacheFile = File(context.cacheDir, cacheFileName)
cacheFile.outputStream().use {
Json.encodeToStream(dataCache, it)
}
}

override suspend fun deleteWorksitesSecondaryData(
incidentId: Long,
pageIndex: Int,
) {
val cacheFileName = secondaryDataFileName(incidentId, pageIndex)
deleteCacheFile(cacheFileName)
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package com.crisiscleanup.core.data

import com.crisiscleanup.core.common.AppMemoryStats
import com.crisiscleanup.core.common.LocationProvider
import com.crisiscleanup.core.common.log.AppLogger
import com.crisiscleanup.core.common.log.CrisisCleanupLoggers
import com.crisiscleanup.core.common.log.CrisisCleanupLoggers.Worksites
import com.crisiscleanup.core.common.log.Logger
import com.crisiscleanup.core.data.model.asEntities
import com.crisiscleanup.core.data.util.IncidentDataPullStats
import com.crisiscleanup.core.database.dao.IncidentDao
import com.crisiscleanup.core.database.dao.WorksiteDao
import com.crisiscleanup.core.database.dao.WorksiteDaoPlus
import com.crisiscleanup.core.database.dao.WorksiteSyncStatDao
import com.crisiscleanup.core.database.model.BoundedSyncedWorksiteIds
import com.crisiscleanup.core.database.model.CoordinateGridQuery
import com.crisiscleanup.core.database.model.IncidentWorksitesFullSyncStatsEntity
import com.crisiscleanup.core.database.model.PopulatedIncidentSyncStats
import com.crisiscleanup.core.database.model.SwNeBounds
import com.crisiscleanup.core.network.CrisisCleanupNetworkDataSource
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
Expand All @@ -35,7 +37,8 @@ data class IncidentProgressPullStats(

interface WorksitesFullSyncer {
val fullPullStats: Flow<IncidentProgressPullStats>

val secondaryDataPullStats: Flow<IncidentDataPullStats>
val onFullDataPullComplete: Flow<Long>
suspend fun sync(incidentId: Long)
}

Expand All @@ -48,11 +51,14 @@ class IncidentWorksitesFullSyncer @Inject constructor(
private val worksiteDao: WorksiteDao,
private val worksiteDaoPlus: WorksiteDaoPlus,
private val worksiteSyncStatDao: WorksiteSyncStatDao,
memoryStats: AppMemoryStats,
private val deviceInspector: SyncCacheDeviceInspector,
private val locationProvider: LocationProvider,
@Logger(CrisisCleanupLoggers.Worksites) private val logger: AppLogger,
private val secondaryDataSyncer: WorksitesSecondaryDataSyncer,
@Logger(Worksites) private val logger: AppLogger,
) : WorksitesFullSyncer {
override val fullPullStats = MutableStateFlow(IncidentProgressPullStats())
override val secondaryDataPullStats = secondaryDataSyncer.dataPullStats
override val onFullDataPullComplete = MutableSharedFlow<Long>()

override suspend fun sync(incidentId: Long) {
worksiteSyncStatDao.getIncidentSyncStats(incidentId)?.let { syncStats ->
Expand All @@ -65,28 +71,28 @@ class IncidentWorksitesFullSyncer @Inject constructor(
longitude = 999.0,
radius = 0.0,
)
saveWorksitesFull(syncStats.entity.targetCount, fullStats)
saveWorksitesFull(
syncStats,
fullStats,
)
}
}
}

private val allWorksitesMemoryThreshold = 100
private val isCapableDevice = memoryStats.availableMemory >= allWorksitesMemoryThreshold

// TODO Use connection strength to determine count
private val fullSyncPageCount = 40
private val idSyncPageCount = 20

private suspend fun saveWorksitesFull(
initialSyncCount: Int,
syncStats: IncidentWorksitesFullSyncStatsEntity,
syncStats: PopulatedIncidentSyncStats,
fullStats: IncidentWorksitesFullSyncStatsEntity,
) = coroutineScope {
val incidentId = syncStats.incidentId
val incidentId = fullStats.incidentId

val locationQueryParameters = syncStats.asQueryParameters(locationProvider)
val pullAll = syncStats.syncedAt == null || locationQueryParameters.hasLocationChange
// TODO Adjust according to strength of network connection in real time
val syncPageCount = fullSyncPageCount * (if (isCapableDevice) 2 else 1)
val locationQueryParameters = fullStats.asQueryParameters(locationProvider)
val pullAll = fullStats.syncedAt == null || locationQueryParameters.hasLocationChange
val pageCountScale = if (deviceInspector.isLimitedDevice) 1 else 2
val syncPageCount = fullSyncPageCount * pageCountScale
val largeIncidentWorksitesCount = syncPageCount * 15

val worksiteCount = networkDataSource.getWorksitesCount(incidentId)
Expand Down Expand Up @@ -135,10 +141,14 @@ class IncidentWorksitesFullSyncer @Inject constructor(
}
if (pagedCount >= worksiteCount) {
worksiteSyncStatDao.upsert(
syncStats.copy(syncedAt = syncStartedAt),
fullStats.copy(syncedAt = syncStartedAt),
)

onFullDataPullComplete.emit(incidentId)
}
} else {
secondaryDataSyncer.sync(incidentId, syncStats)

if (locationQueryParameters.hasLocation) {
val isSynced = saveWorksitesAroundLocation(
incidentId,
Expand All @@ -149,7 +159,7 @@ class IncidentWorksitesFullSyncer @Inject constructor(
}
if (isSynced) {
worksiteSyncStatDao.upsert(
syncStats.copy(
fullStats.copy(
syncedAt = syncStartedAt,
latitude = locationQueryParameters.latitude,
longitude = locationQueryParameters.longitude,
Expand All @@ -160,6 +170,8 @@ class IncidentWorksitesFullSyncer @Inject constructor(
} else {
// TODO Signal user to set sync center and radius
}

onFullDataPullComplete.emit(incidentId)
}
} else {
// val newWorksitesCount = worksiteCount - initialSyncCount
Expand Down
Loading

0 comments on commit 12b50ff

Please sign in to comment.