diff --git a/app-sandbox/build.gradle.kts b/app-sandbox/build.gradle.kts index 01b2cc089..4b357f9c9 100644 --- a/app-sandbox/build.gradle.kts +++ b/app-sandbox/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(libs.androidx.profileinstaller) implementation(libs.coil.kt) + implementation(libs.coil.kt.compose) implementation(libs.kotlinx.coroutines.playservices) implementation(libs.playservices.maps) diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt index 2ea0118a5..038fcaab6 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApp.kt @@ -24,8 +24,9 @@ import androidx.navigation.NavController import com.crisiscleanup.core.designsystem.component.CrisisCleanupBackground import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy -import com.crisiscleanup.sandbox.navigation.MULTI_IMAGE_ROUTE +import com.crisiscleanup.sandbox.navigation.ASYNC_IMAGE_ROUTE import com.crisiscleanup.sandbox.navigation.SandboxNavHost +import com.crisiscleanup.sandbox.navigation.navigateToAsyncImage import com.crisiscleanup.sandbox.navigation.navigateToBottomNav import com.crisiscleanup.sandbox.navigation.navigateToCheckboxes import com.crisiscleanup.sandbox.navigation.navigateToChips @@ -65,7 +66,7 @@ fun SandboxApp( SandboxNavHost( appState.navController, appState::onBack, - MULTI_IMAGE_ROUTE, + ASYNC_IMAGE_ROUTE, ) } } @@ -98,6 +99,9 @@ fun RootRoute(navController: NavController) { CrisisCleanupTextButton(text = "Images") { navController.navigateToMultiImage() } + CrisisCleanupTextButton(text = "Async image") { + navController.navigateToAsyncImage() + } } } } diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt index 93767c37e..1b684171c 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/SandboxApplication.kt @@ -134,8 +134,6 @@ class AppSyncer @Inject constructor() : SyncPuller, SyncPusher { override suspend fun syncPushWorksitesAsync() = CompletableDeferred(SyncResult.NotAttempted("")) - override fun stopPushWorksites() {} - override suspend fun syncPushMedia() = SyncResult.NotAttempted("") override suspend fun syncPushWorksites() = SyncResult.NotAttempted("") diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt index 614b5cafa..4cd9cb055 100644 --- a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/navigation/SandboxNavigation.kt @@ -6,6 +6,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.crisiscleanup.sandbox.RootRoute +import com.crisiscleanup.sandbox.ui.AsyncImageView import com.crisiscleanup.sandbox.ui.BottomNavRoute import com.crisiscleanup.sandbox.ui.CheckboxesRoute import com.crisiscleanup.sandbox.ui.ChipsRoute @@ -18,6 +19,7 @@ private const val CHIPS_ROUTE = "chips" private const val BOTTOM_NAV_ROUTE = "bottom-nav" const val SINGLE_IMAGE_ROUTE = "single-image" const val MULTI_IMAGE_ROUTE = "multi-image" +const val ASYNC_IMAGE_ROUTE = "async-image" fun NavController.navigateToBottomNav() { this.navigate(BOTTOM_NAV_ROUTE) @@ -39,6 +41,10 @@ fun NavController.navigateToMultiImage() { this.navigate(MULTI_IMAGE_ROUTE) } +fun NavController.navigateToAsyncImage() { + this.navigate(ASYNC_IMAGE_ROUTE) +} + @Composable fun SandboxNavHost( navController: NavHostController, @@ -72,5 +78,9 @@ fun SandboxNavHost( composable(MULTI_IMAGE_ROUTE) { MultiImageRoute(onBack) } + + composable(ASYNC_IMAGE_ROUTE) { + AsyncImageView() + } } } diff --git a/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/AsyncImageView.kt b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/AsyncImageView.kt new file mode 100644 index 000000000..e39ffb1cf --- /dev/null +++ b/app-sandbox/src/main/java/com/crisiscleanup/sandbox/ui/AsyncImageView.kt @@ -0,0 +1,51 @@ +package com.crisiscleanup.sandbox.ui + +import android.util.Log +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.crisiscleanup.core.common.R +import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons + +@Composable +fun AsyncImageView() { + val imageUrl = "" + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUrl) + .setHeader("User-Agent", "Mozilla/5.0") + .build(), + modifier = Modifier + .sizeIn(minWidth = 96.dp) + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)), + placeholder = painterResource(R.drawable.cc_grayscale_pin), + error = rememberVectorPainter(CrisisCleanupIcons.SyncProblem), + fallback = rememberVectorPainter(CrisisCleanupIcons.Image), + contentDescription = null, + contentScale = ContentScale.Fit, + onLoading = { loading -> + Log.i("async-image", "Loading image $loading") + }, + onSuccess = { success -> + Log.i("async-image", "Successfully loaded ${success.result}") + }, + onError = { e -> + Log.e( + "async-image", + "Error loading image ${e.result} for $imageUrl", + e.result.throwable, + ) + }, + ) +} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9d8fb95c0..68dfcf679 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ plugins { android { defaultConfig { - val buildVersion = 247 + val buildVersion = 250 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt index 7782f6260..8b48b8dfa 100644 --- a/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt +++ b/app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt @@ -198,6 +198,7 @@ class MainActivityViewModel @Inject constructor( incidentSelector.incidentId .filter { it != EmptyIncident.id } + .distinctUntilChanged() .onEach { sync( forcePullIncidents = false, diff --git a/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/AppTopBarDataProvider.kt b/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/AppTopBarDataProvider.kt index fadff25e3..c065be5e6 100644 --- a/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/AppTopBarDataProvider.kt +++ b/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/AppTopBarDataProvider.kt @@ -43,6 +43,8 @@ class AppTopBarDataProvider( val showHeaderLoading = incidentCacheRepository.isSyncingActiveIncident + val enableIncidentSelect = incidentsRepository.isFirstLoad.map(Boolean::not) + val screenTitle = incidentSelector.incident .map { it.shortName.ifBlank { translator(screenTitleKey) } } .stateIn( diff --git a/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/ui/AppTopBar.kt b/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/ui/AppTopBar.kt index 46d0d6d51..e61973211 100644 --- a/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/ui/AppTopBar.kt +++ b/core/app-component/src/main/kotlin/com/crisiscleanup/core/appcomponent/ui/AppTopBar.kt @@ -28,6 +28,7 @@ fun AppTopBar( val isHeaderLoading by dataProvider.showHeaderLoading.collectAsState(false) val disasterIconResId by dataProvider.disasterIconResId.collectAsStateWithLifecycle() + val enableIncidentSelect by dataProvider.enableIncidentSelect.collectAsState(false) val isAccountExpired by dataProvider.isAccountExpired.collectAsStateWithLifecycle() val profilePictureUri by dataProvider.profilePictureUri.collectAsStateWithLifecycle() @@ -42,6 +43,7 @@ fun AppTopBar( isAccountExpired = isAccountExpired, openAuthentication = openAuthentication, disasterIconResId = disasterIconResId, + enableIncidentSelect = enableIncidentSelect, onOpenIncidents = onOpenIncidents, ) } @@ -60,6 +62,7 @@ internal fun AppTopBar( isAccountExpired: Boolean = false, openAuthentication: () -> Unit = {}, @DrawableRes disasterIconResId: Int = 0, + enableIncidentSelect: Boolean = false, onOpenIncidents: (() -> Unit)? = null, ) { val t = LocalAppTranslator.current @@ -86,6 +89,7 @@ internal fun AppTopBar( title = title, contentDescription = t("nav.change_incident"), isLoading = isAppHeaderLoading, + enabled = enableIncidentSelect, ) } }, diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/sync/Syncer.kt b/core/common/src/main/java/com/crisiscleanup/core/common/sync/Syncer.kt index ed6cdb76c..70d9875a5 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/sync/Syncer.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/sync/Syncer.kt @@ -54,7 +54,6 @@ interface SyncPuller { interface SyncPusher { fun appPushWorksite(worksiteId: Long, scheduleMediaSync: Boolean = false) suspend fun syncPushWorksitesAsync(): Deferred - fun stopPushWorksites() suspend fun syncPushMedia(): SyncResult suspend fun syncPushWorksites(): SyncResult diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataDownloadSpeedMonitor.kt b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataDownloadSpeedMonitor.kt index 24178a291..7a72bd68b 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataDownloadSpeedMonitor.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataDownloadSpeedMonitor.kt @@ -18,6 +18,6 @@ class IncidentDataDownloadSpeedMonitor @Inject constructor() : DataDownloadSpeed override val isSlowSpeed = isSlowSpeedInternal.distinctUntilChanged() override fun onSpeedChange(isSlow: Boolean) { - this.isSlowSpeedInternal.tryEmit(isSlow) + isSlowSpeedInternal.tryEmit(isSlow) } } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataPullReporter.kt b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataPullReporter.kt index 862bfd5b2..742213825 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataPullReporter.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentDataPullReporter.kt @@ -64,15 +64,6 @@ internal class IncidentDataPullStatsUpdater( reportChange(pullStats.copy(savedCount = pullStats.savedCount + count)) } - fun clearStep() { - reportChange( - pullStats.copy( - currentStep = 0, - stepTotal = 0, - ), - ) - } - fun setStep(current: Int, total: Int) { reportChange( pullStats.copy( @@ -82,6 +73,10 @@ internal class IncidentDataPullStatsUpdater( ) } + fun clearStep() { + setStep(0, 0) + } + fun setNotificationMessage(message: String = "") { reportChange(pullStats.copy(notificationMessage = message)) } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentOrganizationsSyncer.kt b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentOrganizationsSyncer.kt index 6f10cfa0c..ee7a99004 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentOrganizationsSyncer.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/incidentcache/IncidentOrganizationsSyncer.kt @@ -31,8 +31,18 @@ class IncidentOrganizationsSyncer @Inject constructor( private val appVersionProvider: AppVersionProvider, @Logger(CrisisCleanupLoggers.Incidents) private val logger: AppLogger, ) : OrganizationsSyncer { + private val organizationFields = listOf( + "id", + "name", + "affiliates", + "is_active", + "primary_location", + "secondary_location", + "type_t", + "primary_contacts", + ) + override suspend fun sync(incidentId: Long) { - // TODO Update stats during pull saveOrganizationsData(incidentId) } @@ -44,11 +54,12 @@ class IncidentOrganizationsSyncer @Inject constructor( var requestedCount = 0 var networkDataOffset = 0 - val pageDataCount = 200 + val pageDataCount = 100 try { while (networkDataOffset < syncCount) { val worksitesRequest = networkDataSource.getIncidentOrganizations( incidentId, + organizationFields, pageDataCount, networkDataOffset, ) diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataPullStats.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataPullStats.kt index 9304e3f0d..70afb170f 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataPullStats.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataPullStats.kt @@ -3,9 +3,6 @@ package com.crisiscleanup.core.data.model import com.crisiscleanup.core.model.data.EmptyIncident import kotlinx.datetime.Clock import kotlinx.datetime.Instant -import kotlin.math.roundToLong -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.seconds enum class IncidentPullDataType { WorksitesCore, @@ -63,17 +60,4 @@ data class IncidentDataPullStats( startProgressAmount } } - - val projectedFinish: Instant - get() { - val now = Clock.System.now() - val delta = now - startTime - val p = progress - if (p <= 0 || delta <= 0.seconds) { - return now.plus(999_999.hours) - } - - val projectedDeltaSeconds = (delta.inWholeSeconds / p).roundToLong().seconds - return startTime.plus(projectedDeltaSeconds) - } } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataSyncParameters.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataSyncParameters.kt index 036035b67..a946144df 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataSyncParameters.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentDataSyncParameters.kt @@ -80,7 +80,7 @@ data class IncidentDataSyncParameters( fun isSignificantChange( other: BoundedRegion, - thresholdMiles: Float = 0.5f, + thresholdMiles: Double = 0.5, ): Boolean { // ~69 miles in 1 degree. 1/69 ~= 0.0145 (degrees). val thresholdDegrees = 0.0145 * thresholdMiles diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentWorksitesSyncParameters.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentWorksitesSyncParameters.kt index 0b0557714..eca7deeb2 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentWorksitesSyncParameters.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/IncidentWorksitesSyncParameters.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json fun IncidentDataSyncParametersEntity.asExternalModel(logger: AppLogger): IncidentDataSyncParameters { - val boundedRegion = if (boundedRegion.isNotBlank()) { + val savedRegion = if (boundedRegion.isNotBlank()) { try { Json.decodeFromString(boundedRegion) } catch (e: Exception) { @@ -28,7 +28,7 @@ fun IncidentDataSyncParametersEntity.asExternalModel(logger: AppLogger): Inciden after = additionalUpdatedAfter, ), ), - boundedRegion = boundedRegion, + boundedRegion = savedRegion, boundedSyncedAt = boundedSyncedAt, ) } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppDataManagementRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppDataManagementRepository.kt index f37f6d610..a475bd7d6 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppDataManagementRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/AppDataManagementRepository.kt @@ -162,10 +162,7 @@ class CrisisCleanupDataManagementRepository @Inject constructor( private suspend fun isSyncPullStopped(): Boolean { val cacheStage = incidentCacheRepository.cacheStage.first() - return setOf( - IncidentCacheStage.Start, - IncidentCacheStage.End, - ).contains(cacheStage) + return !cacheStage.isSyncingStage } private suspend fun clearPersistedAppData() { diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentCacheRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentCacheRepository.kt index e96edbcb6..0782dcec2 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentCacheRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentCacheRepository.kt @@ -5,6 +5,7 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED import com.crisiscleanup.core.common.AppEnv import com.crisiscleanup.core.common.KeyTranslator import com.crisiscleanup.core.common.LocationProvider +import com.crisiscleanup.core.common.combine import com.crisiscleanup.core.common.haversineDistance import com.crisiscleanup.core.common.kmToMiles import com.crisiscleanup.core.common.log.AppLogger @@ -35,7 +36,7 @@ import com.crisiscleanup.core.database.model.WorksiteFormDataEntity import com.crisiscleanup.core.datastore.IncidentCachePreferencesDataSource import com.crisiscleanup.core.datastore.LocalAppPreferencesDataSource import com.crisiscleanup.core.model.data.EmptyIncident -import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.IncidentIdNameType import com.crisiscleanup.core.model.data.IncidentLocationBounder import com.crisiscleanup.core.model.data.IncidentWorksitesCachePreferences import com.crisiscleanup.core.network.CrisisCleanupNetworkDataSource @@ -49,19 +50,20 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.combine as kCombine interface IncidentCacheRepository { val isSyncingActiveIncident: Flow @@ -123,23 +125,53 @@ class IncidentWorksitesCacheRepository @Inject constructor( private val syncPlanReference = AtomicReference(EmptySyncPlan) private val syncingIncidentId = MutableStateFlow(EmptyIncident.id) - override val isSyncingActiveIncident = combine( - incidentSelector.incidentId, + + override val cacheStage = MutableStateFlow(IncidentCacheStage.Inactive) + + private val isSyncingData = cacheStage.map { + it.isSyncingStage + } + + private val isSyncingInitialIncidents = kCombine( syncingIncidentId, + cacheStage, ::Pair, ) - .map { (incidentId, syncingId) -> - incidentId == syncingId + .map { (syncingId, stage) -> + syncingId == EmptyIncident.id && stage == IncidentCacheStage.Incidents } - override val cacheStage = MutableStateFlow(IncidentCacheStage.Start) + private val planSubmissionCountFlow = MutableStateFlow(0) + private val planSubmissionCounter = AtomicInteger(0) + + override val isSyncingActiveIncident = combine( + isSyncingData, + incidentSelector.incidentId, + syncingIncidentId, + planSubmissionCountFlow, + isSyncingInitialIncidents, + ) { isSyncing, incidentId, syncingId, submissionCount, isSyncingInitial -> + Pair( + Triple(isSyncing, incidentId, syncingId), + Pair(submissionCount, isSyncingInitial), + ) + } + .map { data -> + val (isSyncing, incidentId, syncingId) = data.first + val (submissionCount, isSyncingInitial) = data.second + isSyncing && incidentId == syncingId || + submissionCount > 0 || + isSyncingInitial + } override val cachePreferences = incidentCachePreferences.preferences override fun streamSyncStats(incidentId: Long) = - syncParameterDao.streamWorksitesSyncStats(incidentId) + syncParameterDao.streamIncidentDataSyncParameters(incidentId) .map { it?.asExternalModel(appLogger) } + private suspend fun getIncidents() = incidentsRepository.getIncidentsList() + // TODO Write tests override suspend fun submitPlan( overwriteExisting: Boolean, @@ -150,48 +182,54 @@ class IncidentWorksitesCacheRepository @Inject constructor( restartCacheCheckpoint: Boolean, planTimeout: Duration, ): Boolean { - val incidentIds = incidentsRepository.incidents.first() - .map(Incident::id) - .toSet() - val selectedIncident = appPreferences.userData.first().selectedIncidentId - val isIncidentCached = incidentIds.contains(selectedIncident) - - if (incidentIds.isNotEmpty() && - !isIncidentCached && - selectedIncident == EmptyIncident.id && - !forcePullIncidents - ) { - return false - } - - val submittedPlan = IncidentDataSyncPlan( - selectedIncident, - syncIncidents = forcePullIncidents || !isIncidentCached, - syncSelectedIncident = cacheSelectedIncident || !isIncidentCached, - syncActiveIncidentWorksites = cacheActiveIncidentWorksites, - syncWorksitesAdditional = cacheWorksitesAdditional, - restartCache = restartCacheCheckpoint, - ) - synchronized(syncPlanReference) { - if (!overwriteExisting && - !submittedPlan.syncIncidents && - !restartCacheCheckpoint + try { + planSubmissionCountFlow.value = planSubmissionCounter.incrementAndGet() + + val incidentIds = getIncidents() + .map(IncidentIdNameType::id) + .toSet() + val selectedIncidentId = appPreferences.userData.first().selectedIncidentId + val isIncidentCached = incidentIds.contains(selectedIncidentId) + + if (incidentIds.isNotEmpty() && + !isIncidentCached && + selectedIncidentId == EmptyIncident.id && + !forcePullIncidents ) { - with(syncPlanReference.get()) { - if (selectedIncident == incidentId && - submittedPlan.timestamp - timestamp < planTimeout && - submittedPlan.syncSelectedIncidentLevel <= syncSelectedIncidentLevel && - submittedPlan.syncWorksitesLevel <= syncWorksitesLevel - ) { - syncLogger.log("Skipping redundant sync plan for $selectedIncident") - return false + return false + } + + val proposedPlan = IncidentDataSyncPlan( + selectedIncidentId, + syncIncidents = forcePullIncidents || !isIncidentCached, + syncSelectedIncident = cacheSelectedIncident || !isIncidentCached, + syncActiveIncidentWorksites = cacheActiveIncidentWorksites, + syncWorksitesAdditional = cacheWorksitesAdditional, + restartCache = restartCacheCheckpoint, + ) + synchronized(syncPlanReference) { + if (!overwriteExisting && + !proposedPlan.syncIncidents && + !restartCacheCheckpoint + ) { + with(syncPlanReference.get()) { + if (selectedIncidentId == incidentId && + proposedPlan.timestamp - timestamp < planTimeout && + proposedPlan.syncSelectedIncidentLevel <= syncSelectedIncidentLevel && + proposedPlan.syncWorksitesLevel <= syncWorksitesLevel + ) { + syncLogger.log("Skipping redundant sync plan for $selectedIncidentId") + return false + } } } - } - syncLogger.log("Setting sync plan for $selectedIncident") - syncPlanReference.set(submittedPlan) - return true + syncLogger.log("Setting sync plan for $selectedIncidentId") + syncPlanReference.set(proposedPlan) + return true + } + } finally { + planSubmissionCountFlow.value = planSubmissionCounter.decrementAndGet() } } @@ -207,7 +245,10 @@ class IncidentWorksitesCacheRepository @Inject constructor( } val indentation = when (stage) { - IncidentCacheStage.Start -> "" + IncidentCacheStage.Inactive, + IncidentCacheStage.Start, + -> "" + else -> " " } val message = "$indentation$incidentId $stage $details".trimEnd() @@ -241,16 +282,16 @@ class IncidentWorksitesCacheRepository @Inject constructor( override suspend fun sync() = coroutineScope { val syncPlan: IncidentDataSyncPlan + val incidentId: Long synchronized(syncPlanReference) { syncPlan = syncPlanReference.get() + incidentId = syncPlan.incidentId + syncingIncidentId.value = incidentId + logStage(incidentId, IncidentCacheStage.Start) } - val incidentId = syncPlan.incidentId - syncingIncidentId.value = incidentId var incidentName = "" - logStage(incidentId, IncidentCacheStage.Start) - val partialSyncReasons = mutableListOf() try { @@ -265,16 +306,16 @@ class IncidentWorksitesCacheRepository @Inject constructor( val isPaused = syncPreferences.isPaused - val incidents = incidentsRepository.incidents.first() + val incidents = getIncidents() if (incidents.isEmpty()) { return@coroutineScope SyncResult.Error("Failed to sync Incidents") } - val incidentIds = incidents.map(Incident::id).toSet() - if (!incidentIds.contains(incidentId)) { + + incidentName = incidents.firstOrNull { it.id == incidentId }?.name ?: "" + if (incidentName.isBlank()) { return@coroutineScope SyncResult.Partial("Incident not found. Waiting for Incident select.") } - incidentName = incidents.first { it.id == incidentId }.name val worksitesCoreStatsUpdater = IncidentDataPullStatsUpdater { reportStats(syncPlan, it) }.apply { @@ -464,11 +505,11 @@ class IncidentWorksitesCacheRepository @Inject constructor( synchronized(syncPlanReference) { if (syncPlanReference.compareAndSet(syncPlan, EmptySyncPlan)) { cacheStage.value = IncidentCacheStage.End - } - } - if (syncingIncidentId.compareAndSet(incidentId, EmptyIncident.id)) { - logStage(incidentId, IncidentCacheStage.End) + if (syncingIncidentId.compareAndSet(incidentId, EmptyIncident.id)) { + logStage(incidentId, IncidentCacheStage.End) + } + } } syncLogger.flush() @@ -491,16 +532,8 @@ class IncidentWorksitesCacheRepository @Inject constructor( } try { - val recentWorksites = worksitesRepository.getRecentWorksites(incidentId, limit = 3) - if (recentWorksites.isNotEmpty()) { - var totalLatitude = 0.0 - var totalLongitude = 0.0 - recentWorksites.forEach { - totalLatitude += it.latitude - totalLongitude += it.longitude - } - val averageLatitude = totalLatitude / recentWorksites.size - val averageLongitude = totalLongitude / recentWorksites.size + worksitesRepository.getRecentWorksitesCenterLocation(incidentId, limit = 3)?.let { + val (averageLatitude, averageLongitude) = it if (locationBounder.isInBounds( incidentId, latitude = averageLatitude, @@ -646,6 +679,9 @@ class IncidentWorksitesCacheRepository @Inject constructor( } } + // ~60000 Cases longer than 10 mins is reasonably slow + private val slowDownloadSpeed = 100f + private suspend fun cacheBounded( incidentId: Long, isPaused: Boolean, @@ -791,7 +827,7 @@ class IncidentWorksitesCacheRepository @Inject constructor( appLogger.logException(e) "" } - syncParameterDao.updatedBoundedParameters( + syncParameterDao.updateBoundedParameters( incidentId, boundedRegionEncoded, syncStart, @@ -801,9 +837,6 @@ class IncidentWorksitesCacheRepository @Inject constructor( DownloadCountSpeed(savedCount, isSlowDownload) } - // ~60000 Cases longer than 10 mins is reasonably slow - private val slowDownloadSpeed = 100f - private fun getMaxQueryCount(isAdditionalData: Boolean) = if (isAdditionalData) { if (deviceInspector.isLimitedDevice) 2000 else 6000 } else { @@ -1243,6 +1276,7 @@ class IncidentWorksitesCacheRepository @Inject constructor( } enum class IncidentCacheStage { + Inactive, Start, Incidents, WorksitesBounded, @@ -1254,6 +1288,14 @@ enum class IncidentCacheStage { End, } +private val staticCacheStages = setOf( + IncidentCacheStage.Inactive, + IncidentCacheStage.End, +) + +val IncidentCacheStage.isSyncingStage: Boolean + get() = !staticCacheStages.contains(this) + private data class IncidentDataSyncPlan( // May be a new Incident ID val incidentId: Long, diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt index 4f12d896c..3ffe987b5 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/IncidentsRepository.kt @@ -6,11 +6,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant interface IncidentsRepository { - /** - * Is loading incidents data - */ val isLoading: Flow + val isFirstLoad: Flow + val incidentCount: Long /** diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt index ccd537228..c61891a31 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/LanguageTranslationsRepository.kt @@ -47,8 +47,6 @@ interface LanguageTranslationsRepository : KeyTranslator { suspend fun loadLanguages(force: Boolean = false) - fun setLanguage(key: String = "") - fun setLanguageFromSystem() suspend fun getLanguageOptions(): List @@ -178,7 +176,7 @@ class OfflineFirstLanguageTranslationsRepository @Inject constructor( } } - override fun setLanguage(key: String) { + private fun setLanguage(key: String) { setLanguageJob?.cancel() setLanguageJob = coroutineScope.launch(ioDispatcher) { val languages = supportedLanguages.first() diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt index eaeb00365..145ec559b 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstIncidentsRepository.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.withContext import kotlinx.datetime.Clock @@ -67,6 +68,15 @@ class OfflineFirstIncidentsRepository @Inject constructor( override val isLoading: Flow = isSyncing + override val isFirstLoad = combine( + isSyncing, + incidentDao.streamIncidentCount(), + ::Pair, + ) + .mapLatest { (syncing, count) -> + syncing && count == 0L + } + override val incidentCount: Long get() = incidentDao.getIncidentCount() diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstWorksitesRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstWorksitesRepository.kt index e4d3ea438..917b9e20b 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstWorksitesRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/OfflineFirstWorksitesRepository.kt @@ -20,7 +20,6 @@ import com.crisiscleanup.core.model.data.CasesFilter import com.crisiscleanup.core.model.data.IncidentIdWorksiteCount import com.crisiscleanup.core.model.data.OrganizationLocationAreaBounds import com.crisiscleanup.core.model.data.TableDataWorksite -import com.crisiscleanup.core.model.data.Worksite import com.crisiscleanup.core.model.data.WorksiteSortBy import com.crisiscleanup.core.model.data.getClaimStatus import com.crisiscleanup.core.network.CrisisCleanupNetworkDataSource @@ -65,9 +64,9 @@ class OfflineFirstWorksitesRepository @Inject constructor( ) : WorksitesRepository { override val isDeterminingWorksitesCount = MutableStateFlow(false) - private val orgId = accountDataRepository.accountData.map { it.org.id } + private val organizationId = accountDataRepository.accountData.map { it.org.id } - private val organizationLocationAreaBounds = orgId + private val organizationLocationAreaBounds = organizationId .filter { it > 0 } .flatMapLatest { organizationsRepository.streamPrimarySecondaryAreas(it) @@ -125,7 +124,7 @@ class OfflineFirstWorksitesRepository @Inject constructor( IncidentIdWorksiteCount(id, totalCount, totalCount) } - private val organizationAffiliates = orgId.map { + private val organizationAffiliates = organizationId.map { organizationsRepository.getOrganizationAffiliateIds(it, true).toSet() } .stateIn( @@ -137,14 +136,14 @@ class OfflineFirstWorksitesRepository @Inject constructor( override fun streamLocalWorksite(worksiteId: Long) = worksiteDao.streamLocalWorksite(worksiteId).map { it?.asExternalModel( - orgId.first(), + organizationId.first(), languageTranslationsRepository, ) } override suspend fun getWorksite(worksiteId: Long) = worksiteDao.getWorksite(worksiteId).asExternalModel( - orgId.first(), + organizationId.first(), languageTranslationsRepository, ) .worksite @@ -244,13 +243,25 @@ class OfflineFirstWorksitesRepository @Inject constructor( ) } - override suspend fun getRecentWorksites(incidentId: Long, limit: Int): List { - val orgId = orgId.first() - return recentWorksiteDao.getRecents(incidentId, limit) - .map { - it.asExternalModel(orgId, languageTranslationsRepository) - .worksite + override suspend fun getRecentWorksitesCenterLocation( + incidentId: Long, + limit: Int, + ): Pair? { + val recentWorksites = + recentWorksiteDao.getRecentWorksiteCoordinates(incidentId, limit = limit) + if (recentWorksites.isNotEmpty()) { + var totalLatitude = 0.0 + var totalLongitude = 0.0 + recentWorksites.forEach { + totalLatitude += it.latitude + totalLongitude += it.longitude } + val averageLatitude = totalLatitude / recentWorksites.size + val averageLongitude = totalLongitude / recentWorksites.size + return Pair(averageLatitude, averageLongitude) + } + + return null } override fun getUnsyncedCounts(worksiteId: Long) = diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksitesRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksitesRepository.kt index 994974590..a62316e59 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksitesRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/WorksitesRepository.kt @@ -62,10 +62,10 @@ interface WorksitesRepository { viewStart: Instant, ) - suspend fun getRecentWorksites( + suspend fun getRecentWorksitesCenterLocation( incidentId: Long, limit: Int = 3, - ): List + ): Pair? fun getUnsyncedCounts(worksiteId: Long): List diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDao.kt index 5c7a2bac8..52caa4301 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDao.kt @@ -24,6 +24,10 @@ interface IncidentDao { @Query("SELECT COUNT(*) FROM incidents") fun getIncidentCount(): Long + @Transaction + @Query("SELECT COUNT(*) FROM incidents") + fun streamIncidentCount(): Flow + @Transaction @Query( """ diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDataSyncParameterDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDataSyncParameterDao.kt index da033a74b..4556b7bca 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDataSyncParameterDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/IncidentDataSyncParameterDao.kt @@ -12,7 +12,7 @@ import kotlinx.datetime.Instant interface IncidentDataSyncParameterDao { @Transaction @Query("SELECT * FROM incident_data_sync_parameters WHERE id=:id") - fun streamWorksitesSyncStats(id: Long): Flow + fun streamIncidentDataSyncParameters(id: Long): Flow @Transaction @Query( @@ -92,7 +92,7 @@ interface IncidentDataSyncParameterDao { WHERE id=:incidentId """, ) - fun updatedBoundedParameters( + fun updateBoundedParameters( incidentId: Long, boundedRegion: String, boundedSyncedAt: Instant, diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/RecentWorksiteDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/RecentWorksiteDao.kt index e513b707b..4967663e5 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/RecentWorksiteDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/RecentWorksiteDao.kt @@ -4,9 +4,9 @@ import androidx.room.Dao import androidx.room.Query import androidx.room.Transaction import androidx.room.Upsert -import com.crisiscleanup.core.database.model.PopulatedLocalWorksite import com.crisiscleanup.core.database.model.PopulatedRecentWorksite import com.crisiscleanup.core.database.model.RecentWorksiteEntity +import com.crisiscleanup.core.database.model.WorksiteEntity import kotlinx.coroutines.flow.Flow @Dao @@ -40,10 +40,10 @@ interface RecentWorksiteDao { LIMIT :limit """, ) - fun getRecents( + fun getRecentWorksiteCoordinates( incidentId: Long, limit: Int = 3, - ): List + ): List @Upsert fun upsert(recentWorksite: RecentWorksiteEntity) diff --git a/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppPreferencesDataSource.kt b/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppPreferencesDataSource.kt index 8f71abe8b..f3eed3c9b 100644 --- a/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppPreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/crisiscleanup/core/datastore/LocalAppPreferencesDataSource.kt @@ -42,7 +42,7 @@ class LocalAppPreferencesDataSource @Inject constructor( it.syncAttempt.attemptedCounter, ), - selectedIncidentId = it.selectedIncidentId, + selectedIncidentId = if (it.selectedIncidentId <= 0L) EmptyIncident.id else it.selectedIncidentId, languageKey = it.languageKey, diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt index 5a6ef2c26..c59da0d22 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/icon/CrisisCleanupIcons.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Domain import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Landscape @@ -43,6 +44,7 @@ import androidx.compose.material.icons.filled.Rotate90DegreesCcw import androidx.compose.material.icons.filled.Rotate90DegreesCw import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.SentimentNeutral +import androidx.compose.material.icons.filled.SyncProblem import androidx.compose.material.icons.filled.UnfoldMore import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff @@ -80,6 +82,7 @@ object CrisisCleanupIcons { val ExpandMore = icons.ExpandMore val File = icons.Description val Help = Icons.AutoMirrored.Filled.HelpOutline + val Image = icons.Image val Info = icons.Info val List = Icons.AutoMirrored.Filled.List val Location = icons.LocationOn @@ -102,6 +105,7 @@ object CrisisCleanupIcons { val RotateCcw = icons.Rotate90DegreesCcw val SatelliteMap = icons.Landscape val Search = Icons.Rounded.Search + val SyncProblem = icons.SyncProblem val Team = R.drawable.ic_team val UnfoldMore = icons.UnfoldMore val Visibility = icons.Visibility diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt index fbdd63e80..7ca07fcc1 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseDotProvider.kt @@ -5,6 +5,7 @@ import android.graphics.Canvas import android.graphics.Paint import androidx.collection.LruCache import androidx.compose.ui.geometry.Offset +import androidx.core.graphics.createBitmap import com.crisiscleanup.core.common.AndroidResourceProvider import com.crisiscleanup.core.model.data.WorkTypeStatusClaim import com.crisiscleanup.core.model.data.WorkTypeType @@ -87,7 +88,7 @@ class InMemoryDotProvider @Inject constructor( ): BitmapDescriptor? { val cacheKey = DotCacheKey(statusClaim, isDuplicate, isFilteredOut) synchronized(cache) { - cache.get(cacheKey)?.let { + cache[cacheKey]?.let { return it } } @@ -127,7 +128,7 @@ class InMemoryDotProvider @Inject constructor( dotDrawProperties: DotDrawProperties, ): Bitmap { val bitmapSize = dotDrawProperties.bitmapSizePx.toInt() - val output = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888) + val output = createBitmap(bitmapSize, bitmapSize) val canvas = Canvas(output) val radius = dotDrawProperties.dotDiameterPx * 0.5f @@ -160,7 +161,7 @@ data class DotDrawProperties( fun make( resourceProvider: AndroidResourceProvider, bitmapSizeDp: Float = 8f, - dotDiameterDp: Float = 4f, + dotDiameterDp: Float = 5f, strokeWidthDp: Float = 0.5f, ) = DotDrawProperties( bitmapSizePx = resourceProvider.dpToPx(bitmapSizeDp), diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt index 781682049..8c0322125 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/CrisisCleanupNetworkDataSource.kt @@ -60,6 +60,7 @@ interface CrisisCleanupNetworkDataSource { suspend fun getIncidentOrganizations( incidentId: Long, + fields: List, limit: Int, offset: Int, ): NetworkOrganizationsResult @@ -134,11 +135,11 @@ interface CrisisCleanupNetworkDataSource { suspend fun getWorksitesFlagsFormDataPageBefore( incidentId: Long, pageCount: Int, - updatedAfter: Instant, + updatedBefore: Instant, ) = getWorksitesFlagsFormDataPage( incidentId, pageCount, - updatedAfter, + updatedBefore, true, ) diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt b/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt index f037070d0..ae0fcd122 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/retrofit/DataApiClient.kt @@ -65,7 +65,7 @@ private interface DataSourceApi { @TokenAuthenticationHeader @GET("incidents") suspend fun getIncidents( - @Query("fields") + @Query("fields", encoded = true) fields: String, @Query("limit") limit: Int, @@ -78,7 +78,7 @@ private interface DataSourceApi { @GET("incidents") suspend fun getIncidentsNoAuth( - @Query("fields") + @Query("fields", encoded = true) fields: String, @Query("limit") limit: Int, @@ -91,7 +91,7 @@ private interface DataSourceApi { @GET("incidents_list") suspend fun getIncidentsList( - @Query("fields") + @Query("fields", encoded = true) fields: String, @Query("limit") limit: Int, @@ -114,15 +114,19 @@ private interface DataSourceApi { suspend fun getIncident( @Path("id") id: Long, - @Query("fields") + @Query("fields", encoded = true) fields: String, ): NetworkIncidentResult @TokenAuthenticationHeader - @GET("incidents/{incidentId}/organizations") + @ConnectTimeoutHeader("5") + @ReadTimeoutHeader("10") + @GET("organizations") suspend fun getIncidentOrganizations( - @Path("incidentId") + @Query("incident") incidentId: Long, + @Query("fields", encoded = true) + fields: String, @Query("limit") limit: Int, @Query("offset") @@ -138,7 +142,7 @@ private interface DataSourceApi { limit: Int, @Query("offset") offset: Int, - @Query("fields") + @Query("fields", encoded = true) fields: String?, ): NetworkWorksitesCoreDataResult @@ -158,7 +162,7 @@ private interface DataSourceApi { suspend fun getWorksitesLocationSearch( @Query("incident") incidentId: Long, - @Query("fields") + @Query("fields", encoded = true) fields: String, @Query("search") q: String, @@ -179,7 +183,7 @@ private interface DataSourceApi { @ReadTimeoutHeader("15") @GET("worksites") suspend fun getWorksite( - @Query("id__in") + @Query("id__in", encoded = true) worksiteId: String, @Tag endpointId: EndpointRequestId = EndpointRequestId.Worksite, ): NetworkWorksitesFullResult @@ -262,7 +266,7 @@ private interface DataSourceApi { @TokenAuthenticationHeader @GET("organizations") suspend fun getNearbyClaimingOrganizations( - @Query("nearby_claimed") + @Query("nearby_claimed", encoded = true) nearbyClaimed: String, ): NetworkOrganizationsResult @@ -310,7 +314,7 @@ private interface DataSourceApi { @Query("limit") limit: Int, @Query("updated_at__lt") - updatedAtBefore: Instant?, + updatedAtBefore: Instant, @Query("sort") sort: String, ): NetworkFlagsFormDataResult @@ -453,9 +457,15 @@ class DataApiClient @Inject constructor( override suspend fun getIncidentOrganizations( incidentId: Long, + fields: List, limit: Int, offset: Int, - ) = networkApi.getIncidentOrganizations(incidentId, limit, offset) + ) = networkApi.getIncidentOrganizations( + incidentId, + fields.joinToString(","), + limit = limit, + offset = offset, + ) .apply { errors?.tryThrowException() } override suspend fun getWorksitesCoreData(incidentId: Long, limit: Int, offset: Int) = diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt index 5ba5afbb5..f0881eb05 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/CaseMediaViews.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity @@ -332,8 +333,9 @@ internal fun PhotosSection( contentSize = it.size.toSize() }, ) { + val photoUri = photo.thumbnailUri.ifBlank { photo.imageUri } AsyncImage( - model = photo.thumbnailUri.ifBlank { photo.imageUri }, + model = photoUri, modifier = Modifier .sizeIn(minWidth = 96.dp) .fillMaxSize() @@ -344,6 +346,8 @@ internal fun PhotosSection( } }, placeholder = painterResource(commonR.drawable.cc_grayscale_pin), + error = rememberVectorPainter(CrisisCleanupIcons.SyncProblem), + fallback = rememberVectorPainter(CrisisCleanupIcons.Image), contentDescription = photo.title, contentScale = ContentScale.Crop, ) diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt index e1e7ffdda..c03ea361e 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/CasesViewModel.kt @@ -37,6 +37,7 @@ import com.crisiscleanup.core.data.model.IncidentDataPullStats import com.crisiscleanup.core.data.model.IncidentPullDataType import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.CasesFilterRepository +import com.crisiscleanup.core.data.repository.IncidentCacheRepository import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.OrganizationsRepository @@ -101,6 +102,7 @@ class CasesViewModel @Inject constructor( incidentsRepository: IncidentsRepository, incidentBoundsProvider: IncidentBoundsProvider, private val worksitesRepository: WorksitesRepository, + incidentCacheRepository: IncidentCacheRepository, val incidentSelector: IncidentSelector, dataPullReporter: IncidentDataPullReporter, private val mapCaseIconProvider: MapCaseIconProvider, @@ -134,6 +136,13 @@ class CasesViewModel @Inject constructor( coroutineScope = viewModelScope, ) val incidentsData = loadSelectIncidents.data + val enableIncidentSelect = incidentsRepository.isFirstLoad + .map(Boolean::not) + .stateIn( + scope = viewModelScope, + initialValue = false, + started = SharingStarted.WhileSubscribed(), + ) val incidentId: Long get() = incidentSelector.incidentId.value @@ -233,10 +242,9 @@ class CasesViewModel @Inject constructor( * Incident or worksites data are currently saving/caching/loading */ val isLoadingData = combine( - isIncidentLoading, - dataProgress, + incidentCacheRepository.isSyncingActiveIncident, worksitesRepository.isDeterminingWorksitesCount, - ) { b0, progress, b2 -> b0 || progress.isLoadingPrimary || b2 } + ) { b0, b1 -> b0 || b1 } private var _mapCameraZoom = MutableStateFlow(MapViewCameraZoomDefault) val mapCameraZoom = _mapCameraZoom.asStateFlow() diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapBoundsManager.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapBoundsManager.kt index 4e6020caf..c43d9c0d3 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapBoundsManager.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapBoundsManager.kt @@ -18,6 +18,7 @@ import com.google.android.gms.maps.model.LatLngBounds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce @@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.seconds @@ -84,6 +86,11 @@ internal class CasesMapBoundsManager( } } .distinctUntilChanged() + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) private val savedBounds = combine( incidentSelector.incidentId, @@ -169,7 +176,7 @@ internal class CasesMapBoundsManager( mapBoundsCache = bounds - if (isMapLoaded && cacheToDisk) { + if (isStarted && cacheToDisk) { val incidentId = incidentSelector.incidentId.value saveIncidentMapBounds.value = bounds.asIncidentCoordinateBounds(incidentId) } diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapMarkerManager.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapMarkerManager.kt index 82fc0905d..9beebe841 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapMarkerManager.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesMapMarkerManager.kt @@ -65,7 +65,12 @@ internal class CasesMapMarkerManager( } } - return BoundsQueryParams(fullCount, queryCount, sw, ne) + return BoundsQueryParams( + fullCount = fullCount, + queryCount = queryCount, + southWest = sw, + northEast = ne, + ) } suspend fun queryWorksitesInBounds( @@ -86,8 +91,8 @@ internal class CasesMapMarkerManager( ensureActive() - val sw = q.southwest - val ne = q.northeast + val sw = q.southWest + val ne = q.northEast val mapMarks = worksitesRepository.getWorksitesMapVisual( incidentId, sw.latitude, @@ -216,8 +221,8 @@ internal class CasesMapMarkerManager( private data class BoundsQueryParams( val fullCount: Int, val queryCount: Int, - val southwest: LatLng, - val northeast: LatLng, + val southWest: LatLng, + val northEast: LatLng, ) private data class MarkerFromCenter( diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesOverviewMapTileRenderer.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesOverviewMapTileRenderer.kt index 8d9770f45..a37292565 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesOverviewMapTileRenderer.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/map/CasesOverviewMapTileRenderer.kt @@ -79,8 +79,8 @@ class CaseDotsMapTileRenderer @Inject constructor( private var locationCoordinates: Pair? = null override fun setIncident(id: Long, worksitesCount: Int, clearCache: Boolean) { - val isIncidentChanged = id != incidentIdCache synchronized(tileCache) { + val isIncidentChanged = id != incidentIdCache if (isIncidentChanged || clearCache) { tileCache.evictAll() } diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt index 77790b46e..96d8512a1 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreen.kt @@ -133,7 +133,6 @@ internal fun CasesRoute( } val incidentsData by viewModel.incidentsData.collectAsStateWithLifecycle() - val isIncidentLoading by viewModel.isIncidentLoading.collectAsState(true) val isLoadingData by viewModel.isLoadingData.collectAsState(true) if (incidentsData is IncidentsData.Incidents) { val isTableView by viewModel.isTableView.collectAsStateWithLifecycle() @@ -191,7 +190,8 @@ internal fun CasesRoute( } val editedWorksiteLocation = viewModel.editedWorksiteLocation val isMyLocationEnabled = viewModel.isMyLocationEnabled - val hasIncidents = (incidentsData as IncidentsData.Incidents).incidents.isNotEmpty() + + val enableIncidentSelect by viewModel.enableIncidentSelect.collectAsStateWithLifecycle() val onSyncDataDelta = remember(viewModel) { { @@ -232,7 +232,7 @@ internal fun CasesRoute( onTableItemSelect = onTableItemSelect, onSyncDataDelta = onSyncDataDelta, onSyncDataFull = onSyncDataFull, - hasIncidents = hasIncidents, + enableIncidentSelect = enableIncidentSelect, ) if (showChangeIncident) { @@ -261,6 +261,7 @@ internal fun CasesRoute( closeDialog = closePermissionDialog, ) } else { + val isIncidentLoading by viewModel.isIncidentLoading.collectAsState(true) val isLoading = incidentsData is IncidentsData.Loading || isIncidentLoading NoIncidentsScreen( isLoading = isLoading, @@ -380,7 +381,7 @@ internal fun CasesScreen( onTableItemSelect: (Worksite) -> Unit = {}, onSyncDataDelta: () -> Unit = {}, onSyncDataFull: () -> Unit = {}, - hasIncidents: Boolean = false, + enableIncidentSelect: Boolean = false, ) { Box { if (isTableView) { @@ -394,7 +395,7 @@ internal fun CasesScreen( onTableItemSelect = onTableItemSelect, onSyncDataDelta = onSyncDataDelta, onSyncDataFull = onSyncDataFull, - hasIncidents = hasIncidents, + enableIncidentSelect = enableIncidentSelect, ) } else { var isSatelliteMapType by remember { mutableStateOf(false) } @@ -441,7 +442,7 @@ internal fun CasesScreen( disableTableViewActions = isTableDataTransient, onSyncDataDelta = onSyncDataDelta, onSyncDataFull = onSyncDataFull, - hasIncidents = hasIncidents, + enableIncidentSelect = enableIncidentSelect, ) AnimatedVisibility( @@ -617,7 +618,7 @@ private fun CasesOverlayElements( disableTableViewActions: Boolean = false, onSyncDataDelta: () -> Unit = {}, onSyncDataFull: () -> Unit = {}, - hasIncidents: Boolean = false, + enableIncidentSelect: Boolean = false, ) { val translator = LocalAppTranslator.current @@ -646,7 +647,7 @@ private fun CasesOverlayElements( shape = CircleShape, containerColor = incidentDisasterContainerColor, contentColor = incidentDisasterContentColor, - enabled = hasIncidents, + enabled = enableIncidentSelect, ) { Icon( painter = painterResource(disasterResId), diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreenTableView.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreenTableView.kt index fb2d7c580..a8f847e95 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreenTableView.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/ui/CasesScreenTableView.kt @@ -95,7 +95,7 @@ internal fun BoxScope.CasesTableView( onTableItemSelect: (Worksite) -> Unit = {}, onSyncDataDelta: () -> Unit = {}, onSyncDataFull: () -> Unit = {}, - hasIncidents: Boolean = false, + enableIncidentSelect: Boolean = false, ) { val countText by viewModel.casesCountTableText.collectAsStateWithLifecycle() @@ -140,7 +140,7 @@ internal fun BoxScope.CasesTableView( title = selectedIncident.shortName, contentDescription = selectedIncident.shortName, isLoading = isLoadingData, - enabled = hasIncidents, + enabled = enableIncidentSelect, ) Spacer(Modifier.weight(1f)) diff --git a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt index 2df878291..2fbc49936 100644 --- a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt +++ b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/IncidentWorksitesCacheViewModel.kt @@ -67,17 +67,17 @@ class IncidentWorksitesCacheViewModel @Inject constructor( ioDispatcher, ) - val isSyncing = incidentCacheRepository.isSyncingActiveIncident + val syncStage = incidentCacheRepository.cacheStage .stateIn( scope = viewModelScope, - initialValue = false, + initialValue = IncidentCacheStage.Inactive, started = subscribedReplay(), ) - val syncStage = incidentCacheRepository.cacheStage + val isSyncing = incidentCacheRepository.isSyncingActiveIncident .stateIn( scope = viewModelScope, - initialValue = IncidentCacheStage.Start, + initialValue = false, started = subscribedReplay(), ) @@ -94,14 +94,6 @@ class IncidentWorksitesCacheViewModel @Inject constructor( started = subscribedReplay(), ) - val isUpdatingCachePreferences = MutableStateFlow(false) - private val syncCachePreferences = incidentCacheRepository.cachePreferences - .stateIn( - scope = viewModelScope, - initialValue = InitialIncidentWorksitesCachePreferences, - started = subscribedReplay(), - ) - private var isUserActed = AtomicBoolean(false) val editingPreferences = MutableStateFlow(InitialIncidentWorksitesCachePreferences) @@ -113,7 +105,7 @@ class IncidentWorksitesCacheViewModel @Inject constructor( private val locationPermissionExpiration = AtomicReference(epochZero) init { - syncCachePreferences + incidentCacheRepository.cachePreferences .onEach { if (editingPreferences.compareAndSet( InitialIncidentWorksitesCachePreferences, @@ -166,12 +158,7 @@ class IncidentWorksitesCacheViewModel @Inject constructor( return@onEach } - isUpdatingCachePreferences.value = true - try { - incidentCacheRepository.updateCachePreferences(it) - } finally { - isUpdatingCachePreferences.value = false - } + incidentCacheRepository.updateCachePreferences(it) } .flowOn(ioDispatcher) .launchIn(externalScope) @@ -216,12 +203,16 @@ class IncidentWorksitesCacheViewModel @Inject constructor( onPreferencesSent() } + private fun pullIncidentData() { + syncPuller.appPullIncidentData(cancelOngoing = true) + } + fun resumeCachingCases() { isUserActed.set(true) updatePreferences(isPaused = false, isRegionBounded = false) - syncPuller.appPullIncidentData(cancelOngoing = true) + pullIncidentData() } fun pauseCachingCases() { @@ -232,6 +223,7 @@ class IncidentWorksitesCacheViewModel @Inject constructor( syncPuller.stopPullWorksites() } + // TODO Simplify state management fun boundCachingCases( isNearMe: Boolean, isUserAction: Boolean = false, @@ -256,14 +248,14 @@ class IncidentWorksitesCacheViewModel @Inject constructor( boundedRegionDataEditor.useMyLocation() } - syncPuller.appPullIncidentData(cancelOngoing = true) + pullIncidentData() } } else { if (isUserAction) { val now = Clock.System.now() val expiration = when (permissionStatus) { - PermissionStatus.Requesting -> now + 10.seconds - PermissionStatus.ShowRationale -> now + 60.seconds + PermissionStatus.Requesting -> now + 20.seconds + PermissionStatus.ShowRationale -> now + 120.seconds else -> epochZero } locationPermissionExpiration.set(expiration) @@ -272,7 +264,7 @@ class IncidentWorksitesCacheViewModel @Inject constructor( } fun resync() { - syncPuller.appPullIncidentData(cancelOngoing = true) + pullIncidentData() } fun resetCaching() { diff --git a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/ui/IncidentWorksitesCacheScreen.kt b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/ui/IncidentWorksitesCacheScreen.kt index c5336a6dd..c13a44877 100644 --- a/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/ui/IncidentWorksitesCacheScreen.kt +++ b/feature/incidentcache/src/main/kotlin/com/crisiscleanup/feature/incidentcache/ui/IncidentWorksitesCacheScreen.kt @@ -89,7 +89,7 @@ private fun IncidentWorksitesCacheScreen( val t = LocalAppTranslator.current val incident by viewModel.incident.collectAsStateWithLifecycle() - val isSyncingIncident by viewModel.isSyncing.collectAsStateWithLifecycle() + val isSyncing by viewModel.isSyncing.collectAsStateWithLifecycle() val syncStage by viewModel.syncStage.collectAsStateWithLifecycle() @@ -141,12 +141,13 @@ private fun IncidentWorksitesCacheScreen( key = "last-synced-info", contentType = "text-item", ) { + val incidentName = incident.shortName val syncedText = lastSynced?.let { t("appCache.synced_incident_as_of_date") - .replace("{incident_name}", incident.shortName) + .replace("{incident_name}", incidentName) .replace("{sync_date}", it) } ?: t("appCache.awaiting_sync_of_incident_name") - .replace("{incident_name}", incident.shortName) + .replace("{incident_name}", incidentName) Text( syncedText, listItemModifier, @@ -160,7 +161,8 @@ private fun IncidentWorksitesCacheScreen( verticalAlignment = Alignment.CenterVertically, ) { val syncStageMessage = when (syncStage) { - IncidentCacheStage.Start -> t("appCache.ready_to_sync") + IncidentCacheStage.Inactive -> t("appCache.ready_to_sync") + IncidentCacheStage.Start -> t("~~Starting sync...") IncidentCacheStage.Incidents -> t("appCache.syncing_incidents") IncidentCacheStage.WorksitesBounded -> t("appCache.syncing_cases_in_designated_area") IncidentCacheStage.WorksitesPreload -> t("appCache.syncing_nearby_cases") @@ -173,7 +175,7 @@ private fun IncidentWorksitesCacheScreen( Text(syncStageMessage) AnimatedBusyIndicator( - isSyncingIncident, + isSyncing, padding = 0.dp, ) } @@ -231,7 +233,7 @@ private fun IncidentWorksitesCacheScreen( textKey = "appCache.choose_area", subTextKey = "appCache.choose_area_description", onSelect = { - viewModel.boundCachingCases(false) + viewModel.boundCachingCases(false, isUserAction = true) scrollToBoundedSection() }, ) @@ -379,7 +381,7 @@ private fun BoundedRegionSection( .height(mapHeightAnimated), ) { val mapWidth = if (contentSize.width > 0) { - val scrollWidth = if (isBoundedRegion && isBoundedByCoordinates) 64.dp else 0.dp + val scrollWidth = if (isBoundedByCoordinates) 72.dp else 0.dp with(density) { contentSize.width.toDp() } - scrollWidth } else { 0.dp @@ -401,7 +403,7 @@ private fun BoundedRegionSection( } }, ) - .align(Alignment.CenterEnd) + .align(Alignment.Center) .animateContentSize() .width(mapWidth), onReleaseMapTouch = { setMovingMap(false) }, diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/ui/MenuScreen.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/ui/MenuScreen.kt index be3776fc5..eecbd543f 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/ui/MenuScreen.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/ui/MenuScreen.kt @@ -60,6 +60,7 @@ import com.crisiscleanup.core.designsystem.component.actionRoundCornerShape import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.cardContainerColor +import com.crisiscleanup.core.designsystem.theme.listItemBottomPadding import com.crisiscleanup.core.designsystem.theme.listItemModifier import com.crisiscleanup.core.designsystem.theme.listItemPadding import com.crisiscleanup.core.designsystem.theme.listItemSpacedBy @@ -162,8 +163,6 @@ private fun MenuScreen( tutorialViewLookup[TutorialViewId.ProvideFeedback] = coordinates.sizePosition } - val isLoadingIncidents by viewModel.isLoadingIncidents.collectAsStateWithLifecycle(false) - var expandHotline by remember { mutableStateOf(false) } val toggleExpandHotline = { expandHotline = !expandHotline } @@ -396,6 +395,7 @@ private fun MenuScreen( } if (showIncidentPicker) { + val isLoadingIncidents by viewModel.isLoadingIncidents.collectAsStateWithLifecycle(false) val closeDialog = { showIncidentPicker = false } val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() val setSelected = remember(viewModel) { @@ -684,8 +684,12 @@ private fun IncidentCacheView( Column(modifier) { if (hasSpeedNotAdaptive) { - Text(t("appMenu.good_internet_use_adaptive")) + Text( + t("appMenu.good_internet_use_adaptive"), + Modifier.listItemBottomPadding(), + ) } + Row( horizontalArrangement = listItemSpacedBy, verticalAlignment = Alignment.CenterVertically, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3c62585f4..770d147ae 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,7 +37,7 @@ androidxUiAutomator = "2.3.0" androidxWindowManager = "1.3.0" androidxWork = "2.9.1" apacheCommonsText = "1.10.0" -coil = "2.6.0" +coil = "2.7.0" dependencyGuard = "0.5.0" firebaseBom = "33.1.2" firebaseCrashlyticsPlugin = "3.0.2" diff --git a/secrets.defaults.properties b/secrets.defaults.properties index 6b60ace5e..86e66a20b 100644 --- a/secrets.defaults.properties +++ b/secrets.defaults.properties @@ -7,4 +7,4 @@ APP_LINK_TLD="localhost" MAPS_API_KEY="create and set key" GETTING_STARTED_VIDEO_URL="https://youtu.be/bFetJpyj4fA?si=R2ky6bN_JGsD26D6" DEBUG_EMAIL_ADDRESS="fill@me.in" -DEBUG_ACCOUNT_PASSWORD="password" +DEBUG_ACCOUNT_PASSWORD="password" \ No newline at end of file diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt b/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt index a1cb56611..b017cda10 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/AppSyncer.kt @@ -72,7 +72,7 @@ class AppSyncer @Inject constructor( accountDataRepository.updateAccountTokens() if (!hasValidAccountTokens()) { - return SyncResult.NotAttempted("Invalid account tokens") + return SyncResult.InvalidAccountTokens } // Other constraints are not important. @@ -201,12 +201,6 @@ class AppSyncer @Inject constructor( } } - private var pushJob: Job? = null - - override fun stopPushWorksites() { - pushJob?.cancel() - } - override fun appPushWorksite(worksiteId: Long, scheduleMediaSync: Boolean) { applicationScope.launch(ioDispatcher) { onSyncPreconditions()?.let { @@ -223,12 +217,8 @@ class AppSyncer @Inject constructor( } } - override suspend fun syncPushWorksitesAsync(): Deferred { - val deferred = applicationScope.async { - syncPushWorksites() - } - pushJob = deferred - return deferred + override suspend fun syncPushWorksitesAsync() = applicationScope.async { + syncPushWorksites() } override suspend fun syncPushWorksites(): SyncResult {