Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d3207d8
Bump version
hueachilles Sep 12, 2025
dd37b1c
Persist map satellite setting
hueachilles Sep 12, 2025
8f09141
Toggle map between normal and satellite across screens
hueachilles Sep 12, 2025
0038748
Update Places SDK deprecations and fix address bug
hueachilles Sep 12, 2025
283d3b0
Determine Incident boundary centroid correctly in random orientations
hueachilles Sep 13, 2025
ebd2db7
Prevent double back navigation at top level navigation
hueachilles Sep 17, 2025
4eeea16
Change inactive duration for performing data clearance
hueachilles Sep 17, 2025
7bd5f4d
Change work view as necessary when centering Case on map
hueachilles Sep 18, 2025
3066eb2
Rebuild FTS tables by app version
hueachilles Sep 18, 2025
85aeac8
Persist toggling of Work screen view
hueachilles Sep 18, 2025
f3035b3
Query and save Incident ignore claiming thresholds
hueachilles Sep 19, 2025
3ff807e
Add database table for storing Case claim thresholds against user+Inc…
hueachilles Sep 19, 2025
f2a7caf
Query and cache Incident claim thresholds
hueachilles Sep 22, 2025
e6f9c45
Update method name to reflect multiple operations
hueachilles Sep 22, 2025
d50d3e1
Start on Incident claim threshold determination
hueachilles Sep 22, 2025
a0f4f11
Claim unclaimed worktypes in edit Case with no other changes
hueachilles Sep 22, 2025
aec25fa
Analyze unsynced work changes by claimed and closed status
hueachilles Sep 22, 2025
9c2962d
Test Incident claim threshold scenarios
hueachilles Sep 23, 2025
feb6eb7
Test unsynced work analyzer
hueachilles Sep 23, 2025
8431b76
Alert and disallow over claiming Cases in view Case screen
hueachilles Sep 24, 2025
3478cdb
Disallow and alert over claiming Cases in create/edit Case
hueachilles Sep 24, 2025
ed85437
Implement interface in sanbox app
hueachilles Sep 24, 2025
5119931
Update from cross development
hueachilles Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ class AppSyncer @Inject constructor() : SyncPuller, SyncPusher {

override suspend fun syncPullStatuses() = SyncResult.NotAttempted("")

override fun appPullAppConfig() {}

override suspend fun syncPullAppConfig() = SyncResult.NotAttempted("")

override fun appPushWorksite(worksiteId: Long, scheduleMediaSync: Boolean) {}

override suspend fun syncPushWorksitesAsync() = CompletableDeferred(SyncResult.NotAttempted(""))
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ plugins {

android {
defaultConfig {
val buildVersion = 277
val buildVersion = 281
applicationId = "com.crisiscleanup"
versionCode = buildVersion
versionName = "0.9.${buildVersion - 168}"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/crisiscleanup/MainActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ class MainActivityViewModel @Inject constructor(
syncFullWorksites = false,
)
accountDataRefresher.updateMyOrganization(true)
accountDataRefresher.updateApprovedIncidents()
accountDataRefresher.updateProfileIncidentsData()

logger.setAccountId(it.id.toString())
} else {
Expand Down Expand Up @@ -206,6 +206,7 @@ class MainActivityViewModel @Inject constructor(

syncPuller.appPullLanguage()
syncPuller.appPullStatuses()
syncPuller.appPullAppConfig()

syncPusher.scheduleSyncMedia()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.crisiscleanup.core.appnav.RouteConstant.ACCOUNT_RESET_PASSWORD_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.ACCOUNT_TRANSFER_ORG_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.CASES_FILTER_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.CASES_GRAPH_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.CASES_SEARCH_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.CASE_ADD_FLAG_ROUTE
Expand All @@ -16,7 +19,15 @@ import com.crisiscleanup.core.appnav.RouteConstant.CASE_EDITOR_SEARCH_ADDRESS_RO
import com.crisiscleanup.core.appnav.RouteConstant.CASE_HISTORY_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.CASE_SHARE_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.INCIDENT_WORKSITES_CACHE_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.INVITE_TEAMMATE_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.LISTS_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.REQUEST_REDEPLOY_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.USER_FEEDBACK_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.VIEW_CASE_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.VIEW_CASE_TRANSFER_WORK_TYPES_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.VIEW_IMAGE_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.VIEW_LIST_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.VIEW_TEAM_ROUTE
import com.crisiscleanup.core.appnav.RouteConstant.WORKSITE_IMAGES_ROUTE
import com.crisiscleanup.core.appnav.navigateToExistingCase
import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier
Expand Down Expand Up @@ -161,13 +172,17 @@ fun CrisisCleanupNavHost(

val searchCasesOnBack = rememberBackOnRoute(navController, onBack, CASES_SEARCH_ROUTE)

val filterCasesOnBack = rememberBackOnRoute(navController, onBack, CASES_FILTER_ROUTE)

val caseEditorOnBack = rememberBackStartingRoute(navController, onBack, CASE_EDITOR_ROUTE)

val searchAddressOnBack =
rememberBackOnRoute(navController, onBack, CASE_EDITOR_SEARCH_ADDRESS_ROUTE)
val moveLocationOnBack =
rememberBackOnRoute(navController, onBack, CASE_EDITOR_MAP_MOVE_LOCATION_ROUTE)

val viewCaseOnBack = rememberBackStartingRoute(navController, onBack, VIEW_CASE_ROUTE)

val transferOnBack =
rememberBackStartingRoute(navController, onBack, VIEW_CASE_TRANSFER_WORK_TYPES_ROUTE)

Expand All @@ -177,13 +192,25 @@ fun CrisisCleanupNavHost(

val historyOnBack = rememberBackOnRoute(navController, onBack, CASE_HISTORY_ROUTE)

val viewTeamOnBack = rememberBackStartingRoute(navController, onBack, VIEW_TEAM_ROUTE)

val viewSingleImageOnBack = rememberBackStartingRoute(navController, onBack, VIEW_IMAGE_ROUTE)
val worksiteImagesOnBack =
rememberBackStartingRoute(navController, onBack, WORKSITE_IMAGES_ROUTE)

val incidentWorksitesCacheOnBack =
rememberBackOnRoute(navController, onBack, INCIDENT_WORKSITES_CACHE_ROUTE)
val openIncidentCache = navController::navigateToIncidentWorksitesCache

val viewListsOnBack = rememberBackOnRoute(navController, onBack, LISTS_ROUTE)
val viewListOnBack = rememberBackStartingRoute(navController, onBack, VIEW_LIST_ROUTE)
val inviteTeammatesOnBack = rememberBackOnRoute(navController, onBack, INVITE_TEAMMATE_ROUTE)
val requestRedeployOnBack = rememberBackOnRoute(navController, onBack, REQUEST_REDEPLOY_ROUTE)
val userFeedbackOnBack = rememberBackOnRoute(navController, onBack, USER_FEEDBACK_ROUTE)

val resetPasswordOnBack = rememberBackOnRoute(navController, onBack, ACCOUNT_RESET_PASSWORD_ROUTE)
val requestAccessOnBack = rememberBackStartingRoute(navController, onBack, ACCOUNT_TRANSFER_ORG_ROUTE)

NavHost(
navController = navController,
startDestination = startDestination,
Expand All @@ -193,11 +220,11 @@ fun CrisisCleanupNavHost(
navController,
nestedGraphs = {
casesSearchScreen(searchCasesOnBack, viewCase)
casesFilterScreen(onBack)
casesFilterScreen(filterCasesOnBack)
caseEditorScreen(navController, caseEditorOnBack)
caseEditSearchAddressScreen(navController, searchAddressOnBack)
caseEditMoveLocationOnMapScreen(moveLocationOnBack)
existingCaseScreen(navController, onBack)
existingCaseScreen(navController, viewCaseOnBack)
existingCaseTransferWorkTypesScreen(transferOnBack)
caseAddFlagScreen(addFlagOnBack, replaceRouteViewCase)
caseShareScreen(shareOnBack)
Expand All @@ -213,7 +240,7 @@ fun CrisisCleanupNavHost(
dashboardScreen()
teamsScreen(
nestedGraphs = {
viewTeamScreen(onBack)
viewTeamScreen(viewTeamOnBack)
},
openAuthentication = openAuthentication,
openViewTeam = navController::navigateToViewTeam,
Expand All @@ -229,29 +256,29 @@ fun CrisisCleanupNavHost(
openSyncLogs = navController::navigateToSyncInsights,
)
incidentWorksitesCache(incidentWorksitesCacheOnBack)
viewSingleImageScreen(onBack)
viewSingleImageScreen(viewSingleImageOnBack)
viewWorksiteImagesScreen(worksiteImagesOnBack)
inviteTeammateScreen(onBack)
requestRedeployScreen(onBack)
userFeedbackScreen(onBack)
listsScreen(navController, onBack)
inviteTeammateScreen(inviteTeammatesOnBack)
requestRedeployScreen(requestRedeployOnBack)
userFeedbackScreen(userFeedbackOnBack)
listsScreen(navController, viewListsOnBack)
viewListScreen(
onBack,
viewListOnBack,
openList = openList,
openWorksite = openViewCase,
)
syncInsightsScreen(viewCase)

resetPasswordScreen(
isAuthenticated = true,
onBack = onBack,
closeResetPassword = onBack,
onBack = resetPasswordOnBack,
closeResetPassword = resetPasswordOnBack,
)

requestAccessScreen(
true,
onBack = onBack,
closeRequestAccess = onBack,
onBack = requestAccessOnBack,
closeRequestAccess = requestAccessOnBack,
openAuth = {},
openForgotPassword = {},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AutocompletePrediction
import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.api.model.PlaceTypes
import com.google.android.libraries.places.api.model.RectangularBounds
import com.google.android.libraries.places.api.model.TypeFilter
import com.google.android.libraries.places.api.net.FetchPlaceRequest
import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRequest
import com.google.android.libraries.places.api.net.PlacesClient
Expand Down Expand Up @@ -143,15 +143,14 @@ class GooglePlaceAddressSearchRepository @Inject constructor(
}

val hasBounds = southwest != null && northeast != null
val bounds =
if (hasBounds) RectangularBounds.newInstance(southwest!!, northeast!!) else null
val bounds = if (hasBounds) RectangularBounds.newInstance(southwest, northeast) else null
val request = FindAutocompletePredictionsRequest.builder()
// Call either setLocationBias() OR setLocationRestriction().
.setLocationBias(bounds)
// .setLocationRestriction(bounds)
.setOrigin(center)
.setCountries(countryCodes)
.setTypesFilter(listOf(TypeFilter.ADDRESS.toString().lowercase()))
.setTypesFilter(listOf(PlaceTypes.ADDRESS))
.setQuery(query)
.setSessionToken(getSessionToken())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ interface SyncPuller {
fun appPullStatuses()
suspend fun syncPullStatuses(): SyncResult

fun appPullAppConfig()
suspend fun syncPullAppConfig(): SyncResult

fun appPullIncidents() = appPullIncidentData(
cancelOngoing = true,
forcePullIncidents = true,
Expand Down
1 change: 1 addition & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ dependencies {
testImplementation(projects.core.testing)
testImplementation(projects.core.datastoreTest)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.crisiscleanup.core.data

import com.crisiscleanup.core.database.dao.WorksiteChangeDao
import com.crisiscleanup.core.model.data.closedWorkTypeStatuses
import com.crisiscleanup.core.model.data.statusFromLiteral
import com.crisiscleanup.core.network.worksitechange.WorkTypeSnapshot
import com.crisiscleanup.core.network.worksitechange.WorksiteChange
import kotlinx.serialization.json.Json
import javax.inject.Inject

interface WorkTypeAnalyzer {
fun countUnsyncedClaimCloseWork(
orgId: Long,
incidentId: Long,
ignoreWorksiteIds: Set<Long>,
): ClaimCloseCounts
}

private val WorkTypeSnapshot.WorkType.isClosed: Boolean
get() {
val workTypeStatus = statusFromLiteral(status)
return closedWorkTypeStatuses.contains(workTypeStatus)
}

class WorksiteChangeWorkTypeAnalyzer @Inject constructor(
private val worksiteChangeDao: WorksiteChangeDao,
) : WorkTypeAnalyzer {
override fun countUnsyncedClaimCloseWork(
orgId: Long,
incidentId: Long,
ignoreWorksiteIds: Set<Long>,
): ClaimCloseCounts {
val worksiteChangesLookup = mutableMapOf<Long, Pair<String, String?>>()
worksiteChangeDao.getOrgChanges(orgId)
.filter {
!ignoreWorksiteIds.contains(it.entity.worksiteId)
}
.onEach {
with(it.entity) {
val entry = worksiteChangesLookup[worksiteId]
worksiteChangesLookup[worksiteId] = if (entry == null) {
Pair(changeData, null)
} else {
Pair(entry.first, changeData)
}
}
}

val workTypeChanges =
mutableListOf<Pair<WorkTypeSnapshot.WorkType, WorkTypeSnapshot.WorkType?>>()

val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
worksiteChangesLookup.forEach { entry ->
val (firstSerializedChange, lastSerializedChange) = entry.value
val firstChange = json.decodeFromString<WorksiteChange>(firstSerializedChange)
firstChange.start?.let { firstSnapshot ->
if (firstSnapshot.core.networkId > 0) {
val lastSnapshot = lastSerializedChange?.let {
json.decodeFromString<WorksiteChange>(lastSerializedChange).change
} ?: firstChange.change
if (lastSnapshot.core.incidentId == incidentId) {
val startWorkLookup = firstSnapshot.workTypes.associateBy { it.localId }
val lastWorkLookup = lastSnapshot.workTypes.associateBy { it.localId }
for ((id, startWorkType) in startWorkLookup) {
// TODO Test coverage on last work type is null
val lastWorkType = lastWorkLookup[id]?.workType
val change = Pair(startWorkType.workType, lastWorkType)
workTypeChanges.add(change)
}
}
}
}
}

var claimCount = 0
var closeCount = 0
for ((startWorkType, lastWorkType) in workTypeChanges) {
val wasClaimed = startWorkType.orgClaim == orgId
val isClaimed = lastWorkType?.orgClaim == orgId
if (wasClaimed != isClaimed) {
claimCount += if (isClaimed) 1 else -1

if (isClaimed && lastWorkType.isClosed) {
closeCount++
}
} else if (isClaimed) {
val wasClosed = startWorkType.isClosed
val isClosed = lastWorkType.isClosed
if (wasClosed != isClosed) {
closeCount += if (isClosed) 1 else -1
}
}
}

return ClaimCloseCounts(claimCount = claimCount, closeCount = closeCount)
}
}

data class ClaimCloseCounts(
val claimCount: Int,
val closeCount: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import com.crisiscleanup.core.data.AppIncidentMapTracker
import com.crisiscleanup.core.data.IncidentMapTracker
import com.crisiscleanup.core.data.IncidentSelectManager
import com.crisiscleanup.core.data.IncidentSelector
import com.crisiscleanup.core.data.WorkTypeAnalyzer
import com.crisiscleanup.core.data.WorksiteChangeWorkTypeAnalyzer
import com.crisiscleanup.core.data.repository.AccountDataRepository
import com.crisiscleanup.core.data.repository.AccountUpdateRepository
import com.crisiscleanup.core.data.repository.AppConfigRepository
import com.crisiscleanup.core.data.repository.AppDataManagementRepository
import com.crisiscleanup.core.data.repository.AppEndOfLifeRepository
import com.crisiscleanup.core.data.repository.AppMetricsRepository
Expand All @@ -16,8 +19,10 @@ import com.crisiscleanup.core.data.repository.CaseHistoryRepository
import com.crisiscleanup.core.data.repository.CasesFilterRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupAccountDataRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupAccountUpdateRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupAppConfigRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupCasesFilterRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupDataManagementRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupIncidentClaimThresholdRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupListsRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupLocalImageRepository
import com.crisiscleanup.core.data.repository.CrisisCleanupOrgVolunteerRepository
Expand All @@ -28,6 +33,7 @@ import com.crisiscleanup.core.data.repository.CrisisCleanupWorkTypeStatusReposit
import com.crisiscleanup.core.data.repository.CrisisCleanupWorksiteChangeRepository
import com.crisiscleanup.core.data.repository.EndOfLifeRepository
import com.crisiscleanup.core.data.repository.IncidentCacheRepository
import com.crisiscleanup.core.data.repository.IncidentClaimThresholdRepository
import com.crisiscleanup.core.data.repository.IncidentWorksitesCacheRepository
import com.crisiscleanup.core.data.repository.IncidentsRepository
import com.crisiscleanup.core.data.repository.LanguageTranslationsRepository
Expand Down Expand Up @@ -215,4 +221,15 @@ interface DataModule {
fun bindsIncidentCacheRepository(
repository: IncidentWorksitesCacheRepository,
): IncidentCacheRepository

@Binds
fun bindsAppConfigRepository(repository: CrisisCleanupAppConfigRepository): AppConfigRepository

@Binds
fun bindsIncidentClaimThresholdRepository(
repository: CrisisCleanupIncidentClaimThresholdRepository,
): IncidentClaimThresholdRepository

@Binds
fun bindsWorkTypeAnalyzer(analyzer: WorksiteChangeWorkTypeAnalyzer): WorkTypeAnalyzer
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fun NetworkIncident.asEntity() = IncidentEntity(
turnOnRelease = turnOnRelease,
isArchived = isArchived ?: false,
type = type,
ignoreClaimingThresholds = ignoreClaimingThresholds ?: false,
)

fun NetworkIncident.locationsAsEntity(): List<IncidentLocationEntity> =
Expand Down
Loading
Loading