diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b44ef03fd..a28b4feb0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,7 +14,7 @@ plugins { android { defaultConfig { - val buildVersion = 252 + val buildVersion = 253 applicationId = "com.crisiscleanup" versionCode = buildVersion versionName = "0.9.${buildVersion - 168}" diff --git a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt b/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt index cf0d1f126..e171b0dab 100644 --- a/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt +++ b/app/src/main/java/com/crisiscleanup/navigation/CrisisCleanupNavHost.kt @@ -35,7 +35,7 @@ import com.crisiscleanup.feature.caseeditor.navigation.navigateToCaseAddFlag import com.crisiscleanup.feature.caseeditor.navigation.navigateToCaseEditor import com.crisiscleanup.feature.caseeditor.navigation.navigateToTransferWorkType import com.crisiscleanup.feature.caseeditor.navigation.navigateToViewCase -import com.crisiscleanup.feature.caseeditor.navigation.rerouteToCaseChange +import com.crisiscleanup.feature.caseeditor.navigation.rerouteToViewCase import com.crisiscleanup.feature.cases.navigation.casesFilterScreen import com.crisiscleanup.feature.cases.navigation.casesGraph import com.crisiscleanup.feature.cases.navigation.casesSearchScreen @@ -133,7 +133,7 @@ fun CrisisCleanupNavHost( } } - val replaceRouteViewCase = navController::rerouteToCaseChange + val replaceRouteViewCase = navController::rerouteToViewCase val openViewCase = remember(navController) { { ids: ExistingWorksiteIdentifier -> diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt index f4d7883a2..41bb98ee0 100644 --- a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -39,7 +39,6 @@ class AndroidFeatureConventionPlugin : Plugin { add("implementation", project(":core:common")) add("implementation", project(":core:data")) add("implementation", project(":core:designsystem")) - add("implementation", project(":core:domain")) add("implementation", project(":core:model")) add("implementation", project(":core:ui")) diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt index e7f1075df..3fa169300 100644 --- a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/KotlinAndroid.kt @@ -29,7 +29,7 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension -internal const val DefaultConfigTargetSdk = 34 +internal const val DefaultConfigTargetSdk = 35 /** * Configure base Kotlin with Android options diff --git a/core/app-component/build.gradle.kts b/core/app-component/build.gradle.kts index a4d752805..a06e3b122 100644 --- a/core/app-component/build.gradle.kts +++ b/core/app-component/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { implementation(projects.core.commoncase) implementation(projects.core.data) implementation(projects.core.designsystem) - implementation(projects.core.domain) implementation(projects.core.selectincident) implementation(libs.androidx.core.ktx) 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 c065be5e6..7286cc2ff 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 @@ -6,8 +6,6 @@ import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.AccountDataRepository 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.domain.LoadSelectIncidents import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map @@ -21,18 +19,9 @@ class AppTopBarDataProvider( incidentSelector: IncidentSelector, private val translator: KeyResourceTranslator, accountDataRepository: AccountDataRepository, - appPreferencesRepository: LocalAppPreferencesRepository, coroutineScope: CoroutineScope, ) { - - val loadSelectIncidents = LoadSelectIncidents( - incidentsRepository = incidentsRepository, - accountDataRepository = accountDataRepository, - incidentSelector = incidentSelector, - appPreferencesRepository = appPreferencesRepository, - coroutineScope = coroutineScope, - ) - val incidentsData = loadSelectIncidents.data + val incidentsData = incidentSelector.data val disasterIconResId = incidentSelector.incident.map { getDisasterIcon(it.disaster) } .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 e61973211..17055bd4c 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 @@ -20,6 +20,7 @@ fun AppTopBar( modifier: Modifier = Modifier, incidentDropdownModifier: Modifier = Modifier, accountToggleModifier: Modifier = Modifier, + incidentSelectTestTag: String = "appIncidentSelect", dataProvider: AppTopBarDataProvider, openAuthentication: () -> Unit = {}, onOpenIncidents: (() -> Unit)? = null, @@ -35,7 +36,11 @@ fun AppTopBar( AppTopBar( modifier = modifier, - incidentDropdownModifier = incidentDropdownModifier, + incidentDropdownModifier = if (enableIncidentSelect) { + incidentDropdownModifier.testTag(incidentSelectTestTag) + } else { + incidentDropdownModifier + }, accountToggleModifier = accountToggleModifier, title = screenTitle, isAppHeaderLoading = isHeaderLoading, @@ -83,7 +88,7 @@ internal fun AppTopBar( TruncatedAppBarText(title = title) } else { IncidentDropdownSelect( - modifier = incidentDropdownModifier.testTag("appIncidentSelector"), + modifier = incidentDropdownModifier, onOpenIncidents, disasterIconResId, title = title, diff --git a/core/common/src/main/java/com/crisiscleanup/core/common/AppVersionProvider.kt b/core/common/src/main/java/com/crisiscleanup/core/common/AppVersionProvider.kt index 200600f5a..33b795088 100644 --- a/core/common/src/main/java/com/crisiscleanup/core/common/AppVersionProvider.kt +++ b/core/common/src/main/java/com/crisiscleanup/core/common/AppVersionProvider.kt @@ -37,7 +37,7 @@ class AndroidAppVersionProvider @Inject constructor( override val version: Pair by lazy { val code = packageInfo.longVersionCode - val name = packageInfo.versionName + val name = packageInfo.versionName!! Pair(code, name) } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelectManager.kt b/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelectManager.kt deleted file mode 100644 index cb4adcc4a..000000000 --- a/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelectManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.crisiscleanup.core.data - -import com.crisiscleanup.core.common.di.ApplicationScope -import com.crisiscleanup.core.model.data.EmptyIncident -import com.crisiscleanup.core.model.data.Incident -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject -import javax.inject.Singleton - -interface IncidentSelector { - val incidentId: StateFlow - - val incident: StateFlow - - suspend fun setIncident(incident: Incident) -} - -@Singleton -class IncidentSelectManager @Inject constructor( - @ApplicationScope coroutineScope: CoroutineScope, -) : IncidentSelector { - override var incident = MutableStateFlow(EmptyIncident) - private set - - override val incidentId = incident.mapLatest { it.id } - .stateIn( - scope = coroutineScope, - initialValue = EmptyIncident.id, - started = SharingStarted.WhileSubscribed(1_000), - ) - - override suspend fun setIncident(incident: Incident) { - this.incident.value = incident - } -} - -@Module -@InstallIn(SingletonComponent::class) -interface IncidentSelectModule { - @Singleton - @Binds - fun bindsIncidentSelector(selector: IncidentSelectManager): IncidentSelector -} diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelector.kt b/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelector.kt new file mode 100644 index 000000000..3434f2bc0 --- /dev/null +++ b/core/data/src/main/java/com/crisiscleanup/core/data/IncidentSelector.kt @@ -0,0 +1,139 @@ +package com.crisiscleanup.core.data + +import com.crisiscleanup.core.common.di.ApplicationScope +import com.crisiscleanup.core.common.subscribedReplay +import com.crisiscleanup.core.data.repository.AccountDataRepository +import com.crisiscleanup.core.data.repository.IncidentsRepository +import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository +import com.crisiscleanup.core.model.data.EmptyIncident +import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.IncidentsData +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +interface IncidentSelector { + val incidentId: StateFlow + + val incident: StateFlow + + val data: StateFlow + + fun selectIncident(incident: Incident) + suspend fun submitIncidentChange(incident: Incident): Boolean +} + +@Singleton +class IncidentSelectManager @Inject constructor( + incidentsRepository: IncidentsRepository, + accountDataRepository: AccountDataRepository, + private val appPreferencesRepository: LocalAppPreferencesRepository, + @ApplicationScope private val coroutineScope: CoroutineScope, +) : IncidentSelector { + private val incidentsSource = combine( + incidentsRepository.incidents, + accountDataRepository.accountData, + ::Pair, + ) + .mapLatest { (incidents, accountData) -> + if (accountData.isCrisisCleanupAdmin) { + incidents + } else { + incidents.filter { accountData.approvedIncidents.contains(it.id) } + } + } + + private val preferencesIncidentId = + appPreferencesRepository.userPreferences.map { + it.selectedIncidentId + } + + private val selectedIncident = combine( + preferencesIncidentId, + incidentsSource, + ::Pair, + ) + .map { (selectedId, incidents) -> + incidents.firstOrNull { it.id == selectedId } + ?: incidents.firstOrNull() + ?: EmptyIncident + } + + override val data = combine( + incidentsSource, + selectedIncident, + ::Pair, + ) + .map { (incidents, selected) -> + if (incidents.isEmpty()) { + IncidentsData.Empty + } else { + IncidentsData.Incidents(incidents, selected) + } + }.stateIn( + scope = coroutineScope, + initialValue = IncidentsData.Loading, + started = subscribedReplay(), + ) + + override var incident = data.mapNotNull { + (it as? IncidentsData.Incidents)?.selected + } + .stateIn( + scope = coroutineScope, + initialValue = EmptyIncident, + started = subscribedReplay(), + ) + + override val incidentId = incident.map { it.id } + .stateIn( + scope = coroutineScope, + initialValue = EmptyIncident.id, + started = subscribedReplay(), + ) + + init { + combine( + preferencesIncidentId, + incidentsSource, + ::Pair, + ) + .onEach { (selectedId, incidents) -> + val selectedIncident = incidents.find { it.id == selectedId } ?: EmptyIncident + if (selectedIncident == EmptyIncident && incidents.isNotEmpty()) { + val firstIncident = incidents[0] + appPreferencesRepository.setSelectedIncident(firstIncident.id) + } + } + .launchIn(coroutineScope) + } + + override fun selectIncident(incident: Incident) { + coroutineScope.launch { + submitIncidentChange(incident) + } + } + + override suspend fun submitIncidentChange(incident: Incident): Boolean { + val incidentId = incident.id + val incidents = incidentsSource.first() + incidents + .find { it.id == incidentId } + ?.let { + appPreferencesRepository.setSelectedIncident(incidentId) + return true + } + + return false + } +} diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/di/DataModule.kt b/core/data/src/main/java/com/crisiscleanup/core/data/di/DataModule.kt index ab448e25a..001c22584 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/di/DataModule.kt @@ -4,6 +4,8 @@ import com.crisiscleanup.core.common.KeyTranslator import com.crisiscleanup.core.common.NetworkMonitor 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.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.AccountUpdateRepository import com.crisiscleanup.core.data.repository.AppDataManagementRepository @@ -70,6 +72,10 @@ interface DataModule { networkMonitor: ConnectivityManagerNetworkMonitor, ): NetworkMonitor + @Singleton + @Binds + fun bindsIncidentSelector(selector: IncidentSelectManager): IncidentSelector + @Singleton @Binds fun bindsLocalAppPreferencesRepository( diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkFile.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkFile.kt index ac862f1e8..0f737203b 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkFile.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkFile.kt @@ -10,7 +10,7 @@ fun NetworkFile.asEntity() = NetworkFileEntity( fileTypeT = fileTypeT, fullUrl = fullUrl, largeThumbnailUrl = largeThumbnailUrl, - mimeContentType = mimeContentType, + mimeContentType = mimeContentType ?: "", smallThumbnailUrl = smallThumbnailUrl, tag = tag, title = title, diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt index f9891d996..922653377 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/model/NetworkWorksite.kt @@ -44,6 +44,9 @@ fun NetworkWorksiteFull.asEntity() = WorksiteEntity( svi = svi, what3Words = what3words, updatedAt = updatedAt, + photoCount = files.map(NetworkFile::mimeContentType) + .filter { it?.startsWith("image/") == true } + .size, ) // Copy similar changes from [NetworkWorksiteFull.asEntity] above @@ -74,6 +77,7 @@ fun NetworkWorksiteCoreData.asEntity() = WorksiteEntity( svi = svi, what3Words = what3words, updatedAt = updatedAt, + photoCount = null, ) fun NetworkWorksiteShort.asEntity() = WorksiteEntity( @@ -105,6 +109,7 @@ fun NetworkWorksiteShort.asEntity() = WorksiteEntity( plusCode = null, reportedBy = null, what3Words = null, + photoCount = null, ) fun NetworkWorksitePage.asEntity() = WorksiteEntity( @@ -136,6 +141,7 @@ fun NetworkWorksitePage.asEntity() = WorksiteEntity( plusCode = plusCode, reportedBy = reportedBy, what3Words = what3words, + photoCount = photoCount, ) fun KeyDynamicValuePair.asWorksiteEntity() = WorksiteFormDataEntity( 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 a475bd7d6..f85142a80 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 @@ -16,9 +16,10 @@ import com.crisiscleanup.core.database.dao.WorksiteDaoPlus import com.crisiscleanup.core.database.dao.fts.rebuildIncidentFts import com.crisiscleanup.core.database.dao.fts.rebuildOrganizationFts import com.crisiscleanup.core.database.dao.fts.rebuildWorksiteTextFts +import com.crisiscleanup.core.model.data.CasesFilter +import com.crisiscleanup.core.model.data.InitialIncidentWorksitesCachePreferences import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -38,6 +39,7 @@ interface AppDataManagementRepository { suspend fun rebuildFts() fun clearAppData() + suspend fun backgroundClearAppData(refreshBackendData: Boolean): Boolean } enum class ClearAppDataStep { @@ -63,13 +65,14 @@ class CrisisCleanupDataManagementRepository @Inject constructor( private val incidentCacheRepository: IncidentCacheRepository, private val languageTranslationsRepository: LanguageTranslationsRepository, private val workTypeStatusRepository: WorkTypeStatusRepository, + private val casesFilterRepository: CasesFilterRepository, + private val appMetricsRepository: LocalAppMetricsRepository, private val accountEventBus: AccountEventBus, @ApplicationScope private val externalScope: CoroutineScope, @Dispatcher(CrisisCleanupDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, @Logger(CrisisCleanupLoggers.App) private val logger: AppLogger, ) : AppDataManagementRepository { - private val _clearingAppDataStep = MutableStateFlow(ClearAppDataStep.None) - override val clearingAppDataStep: Flow = _clearingAppDataStep + override val clearingAppDataStep = MutableStateFlow(ClearAppDataStep.None) override val isAppDataCleared = clearingAppDataStep.map { it == ClearAppDataStep.Cleared } override var clearAppDataError: ClearAppDataStep = ClearAppDataStep.None @@ -84,75 +87,75 @@ class CrisisCleanupDataManagementRepository @Inject constructor( } override fun clearAppData() { - if (!isClearingAppData.compareAndSet(false, true)) { - return + externalScope.launch(ioDispatcher) { + backgroundClearAppData(true) } + } + + override suspend fun backgroundClearAppData(refreshBackendData: Boolean): Boolean = + withContext(ioDispatcher) { + if (!isClearingAppData.compareAndSet(false, true)) { + return@withContext false + } - externalScope.launch(ioDispatcher) { clearAppDataError = ClearAppDataStep.None try { if (incidentsRepository.incidentCount == 0L) { - return@launch + return@withContext true } accountDataRepository.clearAccountTokens() - _clearingAppDataStep.value = ClearAppDataStep.StopSyncPull - externalScope.launch(ioDispatcher) { - stopSyncPull() - } - withContext(Dispatchers.IO) { - for (i in 0..9) { - TimeUnit.SECONDS.sleep(6) - - if (isSyncPullStopped()) { - break - } + clearingAppDataStep.value = ClearAppDataStep.StopSyncPull + stopSyncPull() + for (i in 0..9) { + TimeUnit.SECONDS.sleep(6) + if (isSyncPullStopped()) { + break } } - _clearingAppDataStep.value = ClearAppDataStep.ClearData + clearingAppDataStep.value = ClearAppDataStep.ClearData for (i in 0..<3) { clearPersistedAppData() - withContext(Dispatchers.IO) { - TimeUnit.SECONDS.sleep(2) - } - + TimeUnit.SECONDS.sleep(2) if (isPersistedAppDataCleared()) { break } } - _clearingAppDataStep.value = ClearAppDataStep.FinalClear + clearingAppDataStep.value = ClearAppDataStep.FinalClear - withContext(Dispatchers.IO) { - TimeUnit.SECONDS.sleep(3) - } + TimeUnit.SECONDS.sleep(3) ensureActive() if (!isPersistedAppDataCleared()) { clearAppDataError = ClearAppDataStep.DatabaseNotCleared logger.logCapture("Unable to clear app data") - return@launch + return@withContext false } accountEventBus.onLogout() - _clearingAppDataStep.value = ClearAppDataStep.Cleared + clearingAppDataStep.value = ClearAppDataStep.Cleared - languageTranslationsRepository.loadLanguages(true) - workTypeStatusRepository.loadStatuses(true) + if (refreshBackendData) { + languageTranslationsRepository.loadLanguages(true) + workTypeStatusRepository.loadStatuses(true) + } + + return@withContext true } catch (e: Exception) { logger.logException(e) + return@withContext false } finally { - _clearingAppDataStep.value = ClearAppDataStep.None + clearingAppDataStep.value = ClearAppDataStep.None isClearingAppData.getAndSet(false) } } - } private fun stopSyncPull() { // TODO Stop all including @@ -166,11 +169,14 @@ class CrisisCleanupDataManagementRepository @Inject constructor( } private suspend fun clearPersistedAppData() { - // TODO Clear/reset other persistent data sources relating to incidents databaseOperator.clearBackendDataTables() + // App preferences resets on logout + // Account info clears on logout + casesFilterRepository.changeFilters(CasesFilter()) + incidentCacheRepository.updateCachePreferences(InitialIncidentWorksitesCachePreferences) + appMetricsRepository.setAppOpen() } - private fun isPersistedAppDataCleared() = incidentsRepository.incidentCount == 0L && - worksiteChangeRepository.worksiteChangeCount == 0L && - incidentDataSyncParameterDao.getSyncStatCount() == 0 + private fun isPersistedAppDataCleared() = + incidentsRepository.incidentCount == 0L && worksiteChangeRepository.worksiteChangeCount == 0L && incidentDataSyncParameterDao.getSyncStatCount() == 0 } 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 bb863edb0..0ec74e1bc 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 @@ -762,8 +762,7 @@ class IncidentWorksitesCacheRepository @Inject constructor( !savedWorksiteIds.contains(it.id) } if (deduplicateWorksites.isEmpty()) { - val duplicateCount = networkData.size - deduplicateWorksites.size - log("$duplicateCount duplicate(s), before") + log("${networkData.size} duplicate(s), before") break } @@ -984,8 +983,7 @@ class IncidentWorksitesCacheRepository @Inject constructor( !savedWorksiteIds.contains(it.id) } if (deduplicateWorksites.isEmpty()) { - val duplicateCount = networkData.size - deduplicateWorksites.size - log("$duplicateCount duplicate(s), before") + log("${networkData.size} duplicate(s), before") break } @@ -1086,8 +1084,7 @@ class IncidentWorksitesCacheRepository @Inject constructor( !savedWorksiteIds.contains(it.id) } if (deduplicateWorksites.isEmpty()) { - val duplicateCount = networkData.size - deduplicateWorksites.size - log("$duplicateCount duplicate(s) after") + log("${networkData.size} duplicate(s) after") break } diff --git a/core/data/src/main/java/com/crisiscleanup/core/data/repository/ShareLocationRepository.kt b/core/data/src/main/java/com/crisiscleanup/core/data/repository/ShareLocationRepository.kt index 6ec40e8b8..a8a8d1486 100644 --- a/core/data/src/main/java/com/crisiscleanup/core/data/repository/ShareLocationRepository.kt +++ b/core/data/src/main/java/com/crisiscleanup/core/data/repository/ShareLocationRepository.kt @@ -23,7 +23,7 @@ class CrisisCleanupShareLocationRepository @Inject constructor( private val accountDataRepository: AccountDataRepository, private val appPreferencesRepository: LocalAppPreferencesRepository, - private val appMetricsRepository: AppMetricsRepository, + private val appMetricsRepository: LocalAppMetricsRepository, private val locationProvider: LocationProvider, private val writeApiClient: CrisisCleanupWriteApi, @Logger(CrisisCleanupLoggers.App) private val logger: AppLogger, diff --git a/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/45.json b/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/45.json new file mode 100644 index 000000000..6a7faa8bc --- /dev/null +++ b/core/database/schemas/com.crisiscleanup.core.database.CrisisCleanupDatabase/45.json @@ -0,0 +1,3377 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "517fd23338868bdba39991ddcf9f7c5f", + "entities": [ + { + "tableName": "work_type_statuses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`status` TEXT NOT NULL, `name` TEXT NOT NULL, `list_order` INTEGER NOT NULL, `primary_state` TEXT NOT NULL, PRIMARY KEY(`status`))", + "fields": [ + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryState", + "columnName": "primary_state", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "status" + ] + }, + "indices": [ + { + "name": "index_work_type_statuses_list_order", + "unique": false, + "columnNames": [ + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_type_statuses_list_order` ON `${TABLE_NAME}` (`list_order`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "incidents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `start_at` INTEGER NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL DEFAULT '', `case_label` TEXT NOT NULL DEFAULT '', `incident_type` TEXT NOT NULL DEFAULT '', `active_phone_number` TEXT DEFAULT '', `turn_on_release` INTEGER NOT NULL DEFAULT 0, `is_archived` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startAt", + "columnName": "start_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "caseLabel", + "columnName": "case_label", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "incident_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "activePhoneNumber", + "columnName": "active_phone_number", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "turnOnRelease", + "columnName": "turn_on_release", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isArchived", + "columnName": "is_archived", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "idx_newest_to_oldest_incidents", + "unique": false, + "columnNames": [ + "start_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_newest_to_oldest_incidents` ON `${TABLE_NAME}` (`start_at` DESC)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "incident_locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `location` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "incident_to_incident_location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `incident_location_id` INTEGER NOT NULL, PRIMARY KEY(`incident_id`, `incident_location_id`), FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`incident_location_id`) REFERENCES `incident_locations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentLocationId", + "columnName": "incident_location_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id", + "incident_location_id" + ] + }, + "indices": [ + { + "name": "idx_incident_location_to_incident", + "unique": false, + "columnNames": [ + "incident_location_id", + "incident_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_incident_location_to_incident` ON `${TABLE_NAME}` (`incident_location_id`, `incident_id`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "incident_locations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_location_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_form_fields", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `label` TEXT NOT NULL, `html_type` TEXT NOT NULL, `data_group` TEXT NOT NULL, `help` TEXT DEFAULT '', `placeholder` TEXT DEFAULT '', `read_only_break_glass` INTEGER NOT NULL, `values_default_json` TEXT DEFAULT '', `is_checkbox_default_true` INTEGER DEFAULT 0, `order_label` INTEGER NOT NULL DEFAULT -1, `validation` TEXT DEFAULT '', `recur_default` TEXT DEFAULT '0', `values_json` TEXT DEFAULT '', `is_required` INTEGER DEFAULT 0, `is_read_only` INTEGER DEFAULT 0, `list_order` INTEGER NOT NULL, `is_invalidated` INTEGER NOT NULL, `field_key` TEXT NOT NULL, `field_parent_key` TEXT DEFAULT '', `parent_key` TEXT NOT NULL DEFAULT '', `selected_toggle_work_type` TEXT DEFAULT '', PRIMARY KEY(`incident_id`, `parent_key`, `field_key`), FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "htmlType", + "columnName": "html_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dataGroup", + "columnName": "data_group", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "help", + "columnName": "help", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "placeholder", + "columnName": "placeholder", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "readOnlyBreakGlass", + "columnName": "read_only_break_glass", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valuesDefaultJson", + "columnName": "values_default_json", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "isCheckboxDefaultTrue", + "columnName": "is_checkbox_default_true", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "orderLabel", + "columnName": "order_label", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "validation", + "columnName": "validation", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "recurDefault", + "columnName": "recur_default", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "'0'" + }, + { + "fieldPath": "valuesJson", + "columnName": "values_json", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "isRequired", + "columnName": "is_required", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isInvalidated", + "columnName": "is_invalidated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fieldKey", + "columnName": "field_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fieldParentKey", + "columnName": "field_parent_key", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "parentKeyNonNull", + "columnName": "parent_key", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "selectToggleWorkType", + "columnName": "selected_toggle_work_type", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id", + "parent_key", + "field_key" + ] + }, + "indices": [ + { + "name": "index_incident_form_fields_data_group_parent_key_list_order", + "unique": false, + "columnNames": [ + "data_group", + "parent_key", + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_incident_form_fields_data_group_parent_key_list_order` ON `${TABLE_NAME}` (`data_group`, `parent_key`, `list_order`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `shape_type` TEXT NOT NULL DEFAULT '', `coordinates` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shapeType", + "columnName": "shape_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "coordinates", + "columnName": "coordinates", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "worksite_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `sync_start` INTEGER NOT NULL DEFAULT 0, `target_count` INTEGER NOT NULL, `paged_count` INTEGER NOT NULL DEFAULT 0, `successful_sync` INTEGER, `attempted_sync` INTEGER, `attempted_counter` INTEGER NOT NULL, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`))", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStart", + "columnName": "sync_start", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pagedCount", + "columnName": "paged_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptedSync", + "columnName": "attempted_sync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptedCounter", + "columnName": "attempted_counter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "worksites_root", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sync_uuid` TEXT NOT NULL DEFAULT '', `local_modified_at` INTEGER NOT NULL DEFAULT 0, `synced_at` INTEGER NOT NULL DEFAULT 0, `local_global_uuid` TEXT NOT NULL DEFAULT '', `is_local_modified` INTEGER NOT NULL DEFAULT 0, `sync_attempt` INTEGER NOT NULL DEFAULT 0, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncUuid", + "columnName": "sync_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "localModifiedAt", + "columnName": "local_modified_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isLocalModified", + "columnName": "is_local_modified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncAttempt", + "columnName": "sync_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksites_root_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksites_root_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_worksites_root_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_root_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_worksites_root_is_local_modified_local_modified_at", + "unique": false, + "columnNames": [ + "is_local_modified", + "local_modified_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_root_is_local_modified_local_modified_at` ON `${TABLE_NAME}` (`is_local_modified` DESC, `local_modified_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, `address` TEXT NOT NULL, `auto_contact_frequency_t` TEXT, `case_number` TEXT NOT NULL, `case_number_order` INTEGER NOT NULL DEFAULT 0, `city` TEXT NOT NULL, `county` TEXT NOT NULL, `created_at` INTEGER, `email` TEXT DEFAULT '', `favorite_id` INTEGER, `key_work_type_type` TEXT NOT NULL DEFAULT '', `key_work_type_org` INTEGER, `key_work_type_status` TEXT NOT NULL DEFAULT '', `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, `name` TEXT NOT NULL, `phone1` TEXT, `phone2` TEXT DEFAULT '', `plus_code` TEXT DEFAULT '', `postal_code` TEXT NOT NULL, `reported_by` INTEGER, `state` TEXT NOT NULL, `svi` REAL, `what3Words` TEXT DEFAULT '', `updated_at` INTEGER NOT NULL, `network_photo_count` INTEGER DEFAULT 0, `is_local_favorite` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `worksites_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autoContactFrequencyT", + "columnName": "auto_contact_frequency_t", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "caseNumber", + "columnName": "case_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseNumberOrder", + "columnName": "case_number_order", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "county", + "columnName": "county", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "favoriteId", + "columnName": "favorite_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keyWorkTypeType", + "columnName": "key_work_type_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "keyWorkTypeOrgClaim", + "columnName": "key_work_type_org", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keyWorkTypeStatus", + "columnName": "key_work_type_status", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone1", + "columnName": "phone1", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone2", + "columnName": "phone2", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "plusCode", + "columnName": "plus_code", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "postalCode", + "columnName": "postal_code", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reportedBy", + "columnName": "reported_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "svi", + "columnName": "svi", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "what3Words", + "columnName": "what3Words", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "photoCount", + "columnName": "network_photo_count", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "isLocalFavorite", + "columnName": "is_local_favorite", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksites_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_worksites_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_worksites_incident_id_latitude_longitude", + "unique": false, + "columnNames": [ + "incident_id", + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_latitude_longitude` ON `${TABLE_NAME}` (`incident_id`, `latitude`, `longitude`)" + }, + { + "name": "index_worksites_incident_id_longitude_latitude", + "unique": false, + "columnNames": [ + "incident_id", + "longitude", + "latitude" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_longitude_latitude` ON `${TABLE_NAME}` (`incident_id`, `longitude`, `latitude`)" + }, + { + "name": "index_worksites_incident_id_svi", + "unique": false, + "columnNames": [ + "incident_id", + "svi" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_svi` ON `${TABLE_NAME}` (`incident_id`, `svi`)" + }, + { + "name": "index_worksites_incident_id_updated_at", + "unique": false, + "columnNames": [ + "incident_id", + "updated_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_updated_at` ON `${TABLE_NAME}` (`incident_id`, `updated_at`)" + }, + { + "name": "index_worksites_incident_id_created_at", + "unique": false, + "columnNames": [ + "incident_id", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_created_at` ON `${TABLE_NAME}` (`incident_id`, `created_at`)" + }, + { + "name": "index_worksites_incident_id_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_case_number` ON `${TABLE_NAME}` (`incident_id`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_name_county_city_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "name", + "county", + "city", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_name_county_city_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `name`, `county`, `city`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_city_name_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "city", + "name", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_city_name_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `city`, `name`, `case_number_order`, `case_number`)" + }, + { + "name": "index_worksites_incident_id_county_name_case_number_order_case_number", + "unique": false, + "columnNames": [ + "incident_id", + "county", + "name", + "case_number_order", + "case_number" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksites_incident_id_county_name_case_number_order_case_number` ON `${TABLE_NAME}` (`incident_id`, `county`, `name`, `case_number_order`, `case_number`)" + } + ], + "foreignKeys": [ + { + "table": "worksites_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "work_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER, `claimed_by` INTEGER, `next_recur_at` INTEGER, `phase` INTEGER, `recur` TEXT, `status` TEXT NOT NULL, `work_type` TEXT NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "orgClaim", + "columnName": "claimed_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextRecurAt", + "columnName": "next_recur_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "phase", + "columnName": "phase", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recur", + "columnName": "recur", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "workType", + "columnName": "work_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_work_type", + "unique": true, + "columnNames": [ + "worksite_id", + "work_type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_work_type` ON `${TABLE_NAME}` (`worksite_id`, `work_type`)" + }, + { + "name": "index_work_types_worksite_id_network_id", + "unique": false, + "columnNames": [ + "worksite_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_worksite_id_network_id` ON `${TABLE_NAME}` (`worksite_id`, `network_id`)" + }, + { + "name": "index_work_types_status_worksite_id", + "unique": false, + "columnNames": [ + "status", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_status_worksite_id` ON `${TABLE_NAME}` (`status`, `worksite_id`)" + }, + { + "name": "index_work_types_claimed_by_worksite_id", + "unique": false, + "columnNames": [ + "claimed_by", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_claimed_by_worksite_id` ON `${TABLE_NAME}` (`claimed_by`, `worksite_id`)" + }, + { + "name": "index_work_types_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_work_types_network_id` ON `${TABLE_NAME}` (`network_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_form_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`worksite_id` INTEGER NOT NULL, `field_key` TEXT NOT NULL, `is_bool_value` INTEGER NOT NULL, `value_string` TEXT NOT NULL, `value_bool` INTEGER NOT NULL, PRIMARY KEY(`worksite_id`, `field_key`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fieldKey", + "columnName": "field_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isBoolValue", + "columnName": "is_bool_value", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueString", + "columnName": "value_string", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueBool", + "columnName": "value_bool", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "worksite_id", + "field_key" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_flags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `action` TEXT, `created_at` INTEGER NOT NULL, `is_high_priority` INTEGER DEFAULT 0, `notes` TEXT DEFAULT '', `reason_t` TEXT NOT NULL, `requested_action` TEXT DEFAULT '', FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isHighPriority", + "columnName": "is_high_priority", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + }, + { + "fieldPath": "reasonT", + "columnName": "reason_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "requestedAction", + "columnName": "requested_action", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_flag", + "unique": true, + "columnNames": [ + "worksite_id", + "reason_t" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_flag` ON `${TABLE_NAME}` (`worksite_id`, `reason_t`)" + }, + { + "name": "index_worksite_flags_reason_t", + "unique": false, + "columnNames": [ + "reason_t" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_flags_reason_t` ON `${TABLE_NAME}` (`reason_t`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `local_global_uuid` TEXT NOT NULL DEFAULT '', `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `is_survivor` INTEGER NOT NULL, `note` TEXT NOT NULL DEFAULT '', FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSurvivor", + "columnName": "is_survivor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "unique_worksite_note", + "unique": true, + "columnNames": [ + "worksite_id", + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `unique_worksite_note` ON `${TABLE_NAME}` (`worksite_id`, `network_id`, `local_global_uuid`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "language_translations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `name` TEXT NOT NULL, `translations_json` TEXT, `synced_at` INTEGER DEFAULT 0, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "translationsJson", + "columnName": "translations_json", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sync_logs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `log_time` INTEGER NOT NULL, `log_type` TEXT NOT NULL DEFAULT '', `message` TEXT NOT NULL, `details` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logTime", + "columnName": "log_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "logType", + "columnName": "log_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "details", + "columnName": "details", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sync_logs_log_time", + "unique": false, + "columnNames": [ + "log_time" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sync_logs_log_time` ON `${TABLE_NAME}` (`log_time` DESC)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "worksite_changes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `app_version` INTEGER NOT NULL, `organization_id` INTEGER NOT NULL, `worksite_id` INTEGER NOT NULL, `sync_uuid` TEXT NOT NULL DEFAULT '', `change_model_version` INTEGER NOT NULL, `change_data` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `save_attempt` INTEGER NOT NULL DEFAULT 0, `archive_action` TEXT NOT NULL, `save_attempt_at` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`worksite_id`) REFERENCES `worksites_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appVersion", + "columnName": "app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncUuid", + "columnName": "sync_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "changeModelVersion", + "columnName": "change_model_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changeData", + "columnName": "change_data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "saveAttempt", + "columnName": "save_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "archiveAction", + "columnName": "archive_action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "saveAttemptAt", + "columnName": "save_attempt_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_changes_worksite_id_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_created_at` ON `${TABLE_NAME}` (`worksite_id`, `created_at`)" + }, + { + "name": "index_worksite_changes_worksite_id_save_attempt", + "unique": false, + "columnNames": [ + "worksite_id", + "save_attempt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_save_attempt` ON `${TABLE_NAME}` (`worksite_id`, `save_attempt`)" + }, + { + "name": "index_worksite_changes_worksite_id_save_attempt_at_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "save_attempt_at", + "created_at" + ], + "orders": [ + "ASC", + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_changes_worksite_id_save_attempt_at_created_at` ON `${TABLE_NAME}` (`worksite_id` ASC, `save_attempt_at` ASC, `created_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "worksites_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_organizations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `primary_location` INTEGER, `secondary_location` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primaryLocation", + "columnName": "primary_location", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "secondaryLocation", + "columnName": "secondary_location", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "person_contacts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `first_name` TEXT NOT NULL, `last_name` TEXT NOT NULL, `email` TEXT NOT NULL, `mobile` TEXT NOT NULL, `profilePictureUri` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "first_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "last_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mobile", + "columnName": "mobile", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUri", + "columnName": "profilePictureUri", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "organization_to_primary_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`organization_id` INTEGER NOT NULL, `contact_id` INTEGER NOT NULL, PRIMARY KEY(`organization_id`, `contact_id`), FOREIGN KEY(`organization_id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contact_id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contact_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "organization_id", + "contact_id" + ] + }, + "indices": [ + { + "name": "idx_contact_to_organization", + "unique": false, + "columnNames": [ + "contact_id", + "organization_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_contact_to_organization` ON `${TABLE_NAME}` (`contact_id`, `organization_id`)" + } + ], + "foreignKeys": [ + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "organization_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contact_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "organization_to_affiliate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `affiliate_id` INTEGER NOT NULL, PRIMARY KEY(`id`, `affiliate_id`), FOREIGN KEY(`id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "affiliateId", + "columnName": "affiliate_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "affiliate_id" + ] + }, + "indices": [ + { + "name": "index_organization_to_affiliate_affiliate_id_id", + "unique": false, + "columnNames": [ + "affiliate_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_organization_to_affiliate_affiliate_id_id` ON `${TABLE_NAME}` (`affiliate_id`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_organization_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `target_count` INTEGER NOT NULL, `successful_sync` INTEGER, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`))", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_worksites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `incident_id` INTEGER NOT NULL, `viewed_at` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewedAt", + "columnName": "viewed_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_recent_worksites_incident_id_viewed_at", + "unique": false, + "columnNames": [ + "incident_id", + "viewed_at" + ], + "orders": [ + "ASC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recent_worksites_incident_id_viewed_at` ON `${TABLE_NAME}` (`incident_id` ASC, `viewed_at` DESC)" + }, + { + "name": "index_recent_worksites_viewed_at", + "unique": false, + "columnNames": [ + "viewed_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recent_worksites_viewed_at` ON `${TABLE_NAME}` (`viewed_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_work_type_requests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `worksite_id` INTEGER NOT NULL, `work_type` TEXT NOT NULL, `reason` TEXT NOT NULL, `by_org` INTEGER NOT NULL, `to_org` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `approved_at` INTEGER, `rejected_at` INTEGER, `approved_rejected_reason` TEXT NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "workType", + "columnName": "work_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "byOrg", + "columnName": "by_org", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "toOrg", + "columnName": "to_org", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "approvedAt", + "columnName": "approved_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rejectedAt", + "columnName": "rejected_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "approvedRejectedReason", + "columnName": "approved_rejected_reason", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_work_type_requests_worksite_id_work_type_by_org", + "unique": true, + "columnNames": [ + "worksite_id", + "work_type", + "by_org" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksite_work_type_requests_worksite_id_work_type_by_org` ON `${TABLE_NAME}` (`worksite_id`, `work_type`, `by_org`)" + }, + { + "name": "index_worksite_work_type_requests_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_work_type_requests_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_worksite_work_type_requests_worksite_id_by_org", + "unique": false, + "columnNames": [ + "worksite_id", + "by_org" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_work_type_requests_worksite_id_by_org` ON `${TABLE_NAME}` (`worksite_id`, `by_org`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "network_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `file_id` INTEGER NOT NULL DEFAULT 0, `file_type_t` TEXT NOT NULL, `full_url` TEXT, `large_thumbnail_url` TEXT, `mime_content_type` TEXT NOT NULL, `small_thumbnail_url` TEXT, `tag` TEXT, `title` TEXT, `url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fileId", + "columnName": "file_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "fileTypeT", + "columnName": "file_type_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullUrl", + "columnName": "full_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "largeThumbnailUrl", + "columnName": "large_thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mimeContentType", + "columnName": "mime_content_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "smallThumbnailUrl", + "columnName": "small_thumbnail_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "worksite_to_network_file", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`worksite_id` INTEGER NOT NULL, `network_file_id` INTEGER NOT NULL, PRIMARY KEY(`worksite_id`, `network_file_id`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`network_file_id`) REFERENCES `network_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkFileId", + "columnName": "network_file_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "worksite_id", + "network_file_id" + ] + }, + "indices": [ + { + "name": "index_worksite_to_network_file_network_file_id_worksite_id", + "unique": false, + "columnNames": [ + "network_file_id", + "worksite_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_worksite_to_network_file_network_file_id_worksite_id` ON `${TABLE_NAME}` (`network_file_id`, `worksite_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "network_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "network_file_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "network_file_local_images", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `is_deleted` INTEGER NOT NULL, `rotate_degrees` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `network_files`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDeleted", + "columnName": "is_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rotateDegrees", + "columnName": "rotate_degrees", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_network_file_local_images_is_deleted", + "unique": false, + "columnNames": [ + "is_deleted" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_network_file_local_images_is_deleted` ON `${TABLE_NAME}` (`is_deleted`)" + } + ], + "foreignKeys": [ + { + "table": "network_files", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "worksite_local_images", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `worksite_id` INTEGER NOT NULL, `local_document_id` TEXT NOT NULL, `uri` TEXT NOT NULL, `tag` TEXT NOT NULL, `rotate_degrees` INTEGER NOT NULL, FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "documentId", + "columnName": "local_document_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rotateDegrees", + "columnName": "rotate_degrees", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_worksite_local_images_worksite_id_local_document_id", + "unique": true, + "columnNames": [ + "worksite_id", + "local_document_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_worksite_local_images_worksite_id_local_document_id` ON `${TABLE_NAME}` (`worksite_id`, `local_document_id`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_worksites_full_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `synced_at` INTEGER, `center_my_location` INTEGER NOT NULL, `center_latitude` REAL NOT NULL DEFAULT 999, `center_longitude` REAL NOT NULL DEFAULT 999, `query_area_radius` REAL NOT NULL, PRIMARY KEY(`incident_id`), FOREIGN KEY(`incident_id`) REFERENCES `worksite_sync_stats`(`incident_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isMyLocationCentered", + "columnName": "center_my_location", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "center_latitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "999" + }, + { + "fieldPath": "longitude", + "columnName": "center_longitude", + "affinity": "REAL", + "notNull": true, + "defaultValue": "999" + }, + { + "fieldPath": "radius", + "columnName": "query_area_radius", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "worksite_sync_stats", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "incident_id" + ] + } + ] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "incidents", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_BEFORE_UPDATE BEFORE UPDATE ON `incidents` BEGIN DELETE FROM `incident_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_BEFORE_DELETE BEFORE DELETE ON `incidents` BEGIN DELETE FROM `incident_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_AFTER_UPDATE AFTER UPDATE ON `incidents` BEGIN INSERT INTO `incident_fts`(`docid`, `name`, `short_name`, `incident_type`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`short_name`, NEW.`incident_type`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_fts_AFTER_INSERT AFTER INSERT ON `incidents` BEGIN INSERT INTO `incident_fts`(`docid`, `name`, `short_name`, `incident_type`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`short_name`, NEW.`incident_type`); END" + ], + "tableName": "incident_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT NOT NULL, `short_name` TEXT NOT NULL DEFAULT '', `incident_type` TEXT NOT NULL DEFAULT '', content=`incidents`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortName", + "columnName": "short_name", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "type", + "columnName": "incident_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "incident_organizations", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_BEFORE_UPDATE BEFORE UPDATE ON `incident_organizations` BEGIN DELETE FROM `incident_organization_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_BEFORE_DELETE BEFORE DELETE ON `incident_organizations` BEGIN DELETE FROM `incident_organization_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_AFTER_UPDATE AFTER UPDATE ON `incident_organizations` BEGIN INSERT INTO `incident_organization_fts`(`docid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_incident_organization_fts_AFTER_INSERT AFTER INSERT ON `incident_organizations` BEGIN INSERT INTO `incident_organization_fts`(`docid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END" + ], + "tableName": "incident_organization_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`name` TEXT NOT NULL, content=`incident_organizations`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "case_history_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `worksite_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `created_by` INTEGER NOT NULL, `event_key` TEXT NOT NULL, `past_tense_t` TEXT NOT NULL, `actor_location_name` TEXT NOT NULL, `recipient_location_name` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`worksite_id`) REFERENCES `worksites`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "worksiteId", + "columnName": "worksite_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventKey", + "columnName": "event_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pastTenseT", + "columnName": "past_tense_t", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actorLocationName", + "columnName": "actor_location_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipientLocationName", + "columnName": "recipient_location_name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_case_history_events_worksite_id_created_by_created_at", + "unique": false, + "columnNames": [ + "worksite_id", + "created_by", + "created_at" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_case_history_events_worksite_id_created_by_created_at` ON `${TABLE_NAME}` (`worksite_id`, `created_by`, `created_at`)" + } + ], + "foreignKeys": [ + { + "table": "worksites", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "worksite_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "case_history_event_attrs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `incident_name` TEXT NOT NULL, `patient_case_number` TEXT, `patient_id` INTEGER NOT NULL, `patient_label_t` TEXT, `patient_location_name` TEXT, `patient_name_t` TEXT, `patient_reason_t` TEXT, `patient_status_name_t` TEXT, `recipient_case_number` TEXT, `recipient_id` INTEGER, `recipient_name` TEXT, `recipient_name_t` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `case_history_events`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "incidentName", + "columnName": "incident_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "patientCaseNumber", + "columnName": "patient_case_number", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patientId", + "columnName": "patient_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "patientLabelT", + "columnName": "patient_label_t", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patientLocationName", + "columnName": "patient_location_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patientNameT", + "columnName": "patient_name_t", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patientReasonT", + "columnName": "patient_reason_t", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patientStatusNameT", + "columnName": "patient_status_name_t", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipientCaseNumber", + "columnName": "recipient_case_number", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipientId", + "columnName": "recipient_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recipientName", + "columnName": "recipient_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "recipientNameT", + "columnName": "recipient_name_t", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "case_history_events", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "person_to_organization", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `organization_id` INTEGER NOT NULL, PRIMARY KEY(`id`, `organization_id`), FOREIGN KEY(`id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`organization_id`) REFERENCES `incident_organizations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "organizationId", + "columnName": "organization_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "organization_id" + ] + }, + "indices": [ + { + "name": "index_person_to_organization_organization_id_id", + "unique": false, + "columnNames": [ + "organization_id", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_person_to_organization_organization_id_id` ON `${TABLE_NAME}` (`organization_id`, `id`)" + } + ], + "foreignKeys": [ + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "incident_organizations", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "organization_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "worksites", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_b_BEFORE_UPDATE BEFORE UPDATE ON `worksites` BEGIN DELETE FROM `worksite_text_fts_b` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_b_BEFORE_DELETE BEFORE DELETE ON `worksites` BEGIN DELETE FROM `worksite_text_fts_b` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_b_AFTER_UPDATE AFTER UPDATE ON `worksites` BEGIN INSERT INTO `worksite_text_fts_b`(`docid`, `address`, `case_number`, `city`, `county`, `email`, `name`, `phone1`, `phone2`) VALUES (NEW.`rowid`, NEW.`address`, NEW.`case_number`, NEW.`city`, NEW.`county`, NEW.`email`, NEW.`name`, NEW.`phone1`, NEW.`phone2`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_worksite_text_fts_b_AFTER_INSERT AFTER INSERT ON `worksites` BEGIN INSERT INTO `worksite_text_fts_b`(`docid`, `address`, `case_number`, `city`, `county`, `email`, `name`, `phone1`, `phone2`) VALUES (NEW.`rowid`, NEW.`address`, NEW.`case_number`, NEW.`city`, NEW.`county`, NEW.`email`, NEW.`name`, NEW.`phone1`, NEW.`phone2`); END" + ], + "tableName": "worksite_text_fts_b", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`address` TEXT NOT NULL, `case_number` TEXT NOT NULL, `city` TEXT NOT NULL, `county` TEXT NOT NULL, `email` TEXT NOT NULL, `name` TEXT NOT NULL, `phone1` TEXT NOT NULL, `phone2` TEXT NOT NULL, content=`worksites`)", + "fields": [ + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseNumber", + "columnName": "case_number", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "city", + "columnName": "city", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "county", + "columnName": "county", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone1", + "columnName": "phone1", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone2", + "columnName": "phone2", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "incident_worksites_secondary_sync_stats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`incident_id` INTEGER NOT NULL, `sync_start` INTEGER NOT NULL DEFAULT 0, `target_count` INTEGER NOT NULL, `paged_count` INTEGER NOT NULL DEFAULT 0, `successful_sync` INTEGER, `attempted_sync` INTEGER, `attempted_counter` INTEGER NOT NULL, `app_build_version_code` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`incident_id`), FOREIGN KEY(`incident_id`) REFERENCES `worksite_sync_stats`(`incident_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncStart", + "columnName": "sync_start", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "targetCount", + "columnName": "target_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pagedCount", + "columnName": "paged_count", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "successfulSync", + "columnName": "successful_sync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptedSync", + "columnName": "attempted_sync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attemptedCounter", + "columnName": "attempted_counter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appBuildVersionCode", + "columnName": "app_build_version_code", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "incident_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "worksite_sync_stats", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "incident_id" + ] + } + ] + }, + { + "tableName": "lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `local_global_uuid` TEXT NOT NULL DEFAULT '', `created_by` INTEGER, `updated_by` INTEGER, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `parent` INTEGER, `name` TEXT NOT NULL, `description` TEXT, `list_order` INTEGER, `tags` TEXT, `model` TEXT NOT NULL, `object_ids` TEXT NOT NULL DEFAULT '', `shared` TEXT NOT NULL, `permissions` TEXT NOT NULL, `incident_id` INTEGER, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "updatedBy", + "columnName": "updated_by", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "objectIds", + "columnName": "object_ids", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "shared", + "columnName": "shared", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_lists_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_lists_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_lists_incident_id_updated_at", + "unique": false, + "columnNames": [ + "incident_id", + "updated_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_incident_id_updated_at` ON `${TABLE_NAME}` (`incident_id` DESC, `updated_at` DESC)" + }, + { + "name": "index_lists_updated_at", + "unique": false, + "columnNames": [ + "updated_at" + ], + "orders": [ + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_updated_at` ON `${TABLE_NAME}` (`updated_at` DESC)" + }, + { + "name": "index_lists_model_updated_at", + "unique": false, + "columnNames": [ + "model", + "updated_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_model_updated_at` ON `${TABLE_NAME}` (`model` DESC, `updated_at` DESC)" + }, + { + "name": "index_lists_parent_list_order", + "unique": false, + "columnNames": [ + "parent", + "list_order" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_lists_parent_list_order` ON `${TABLE_NAME}` (`parent`, `list_order`)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "teams_root", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `local_modified_at` INTEGER NOT NULL DEFAULT 0, `synced_at` INTEGER NOT NULL DEFAULT 0, `local_global_uuid` TEXT NOT NULL DEFAULT '', `is_local_modified` INTEGER NOT NULL DEFAULT 0, `sync_attempt` INTEGER NOT NULL DEFAULT 0, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, FOREIGN KEY(`incident_id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localModifiedAt", + "columnName": "local_modified_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncedAt", + "columnName": "synced_at", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "localGlobalUuid", + "columnName": "local_global_uuid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "isLocalModified", + "columnName": "is_local_modified", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "syncAttempt", + "columnName": "sync_attempt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_teams_root_network_id_local_global_uuid", + "unique": true, + "columnNames": [ + "network_id", + "local_global_uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_teams_root_network_id_local_global_uuid` ON `${TABLE_NAME}` (`network_id`, `local_global_uuid`)" + }, + { + "name": "index_teams_root_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_root_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_teams_root_is_local_modified_local_modified_at", + "unique": false, + "columnNames": [ + "is_local_modified", + "local_modified_at" + ], + "orders": [ + "DESC", + "DESC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_root_is_local_modified_local_modified_at` ON `${TABLE_NAME}` (`is_local_modified` DESC, `local_modified_at` DESC)" + } + ], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "incident_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `network_id` INTEGER NOT NULL DEFAULT -1, `incident_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `notes` TEXT NOT NULL, `color` TEXT NOT NULL, `case_count` INTEGER NOT NULL, `case_complete_count` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `teams_root`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "networkId", + "columnName": "network_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "incidentId", + "columnName": "incident_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "caseCount", + "columnName": "case_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "completeCount", + "columnName": "case_complete_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_teams_incident_id_network_id", + "unique": false, + "columnNames": [ + "incident_id", + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_incident_id_network_id` ON `${TABLE_NAME}` (`incident_id`, `network_id`)" + }, + { + "name": "index_teams_network_id", + "unique": false, + "columnNames": [ + "network_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_network_id` ON `${TABLE_NAME}` (`network_id`)" + }, + { + "name": "index_teams_incident_id_name", + "unique": false, + "columnNames": [ + "incident_id", + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teams_incident_id_name` ON `${TABLE_NAME}` (`incident_id`, `name`)" + } + ], + "foreignKeys": [ + { + "table": "teams_root", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "team_to_primary_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`team_id` INTEGER NOT NULL, `contact_id` INTEGER NOT NULL, PRIMARY KEY(`team_id`, `contact_id`), FOREIGN KEY(`team_id`) REFERENCES `teams`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`contact_id`) REFERENCES `person_contacts`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "teamId", + "columnName": "team_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contactId", + "columnName": "contact_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "team_id", + "contact_id" + ] + }, + "indices": [ + { + "name": "idx_contact_to_team", + "unique": false, + "columnNames": [ + "contact_id", + "team_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_contact_to_team` ON `${TABLE_NAME}` (`contact_id`, `team_id`)" + } + ], + "foreignKeys": [ + { + "table": "teams", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "team_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "person_contacts", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "contact_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "incident_data_sync_parameters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `updated_before` INTEGER NOT NULL, `updated_after` INTEGER NOT NULL, `full_updated_before` INTEGER NOT NULL, `full_updated_after` INTEGER NOT NULL, `bounded_region` TEXT NOT NULL, `bounded_synced_at` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `incidents`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedBefore", + "columnName": "updated_before", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updatedAfter", + "columnName": "updated_after", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalUpdatedBefore", + "columnName": "full_updated_before", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "additionalUpdatedAfter", + "columnName": "full_updated_after", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "boundedRegion", + "columnName": "bounded_region", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "boundedSyncedAt", + "columnName": "bounded_synced_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "incidents", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '517fd23338868bdba39991ddcf9f7c5f')" + ] + } +} \ No newline at end of file diff --git a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt index 8f62744a8..97bb67b8e 100644 --- a/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt +++ b/core/database/src/androidTest/java/com/crisiscleanup/core/database/dao/WorksiteDaoTest.kt @@ -400,6 +400,7 @@ fun testWorksiteEntity( svi = null, what3Words = null, updatedAt = updatedAt, + photoCount = null, ) // Defines all fields setting updated_at to be relative to createdAt @@ -436,6 +437,7 @@ fun testWorksiteFullEntity( svi = 6.235f, what3Words = "what,three,words", updatedAt = createdAt.plus(99.seconds), + photoCount = null, ) // Defines all fields not nullable @@ -472,4 +474,5 @@ fun testWorksiteShortEntity( svi = 0.548f, what3Words = null, updatedAt = createdAt.plus(66.seconds), + photoCount = null, ) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt b/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt index a383f37eb..9cfbd0175 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/CrisisCleanupDatabase.kt @@ -118,7 +118,7 @@ import com.crisiscleanup.core.database.util.InstantConverter TeamMemberCrossRef::class, IncidentDataSyncParametersEntity::class, ], - version = 44, + version = 45, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3, spec = Schema2To3::class), @@ -163,6 +163,7 @@ import com.crisiscleanup.core.database.util.InstantConverter AutoMigration(from = 41, to = 42), AutoMigration(from = 42, to = 43), AutoMigration(from = 43, to = 44), + AutoMigration(from = 44, to = 45), ], exportSchema = true, ) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt index 4e42e705e..02c26ec38 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDao.kt @@ -64,9 +64,9 @@ interface WorksiteDao { """ SELECT w.id, latitude, longitude, - key_work_type_type, key_work_type_org, key_work_type_status, COUNT(wt.id) as work_type_count, + key_work_type_type, key_work_type_org, key_work_type_status, COUNT(wt.id) AS work_type_count, favorite_id, - w.created_at, is_local_favorite, reported_by, svi, updated_at + w.created_at, is_local_favorite, reported_by, svi, updated_at, network_photo_count FROM worksites w LEFT JOIN work_types wt ON w.id=wt.worksite_id WHERE incident_id=:incidentId AND (longitude BETWEEN :longitudeWest AND :longitudeEast) AND @@ -244,7 +244,8 @@ interface WorksiteDao { state =:state, svi =:svi, what3Words =COALESCE(:what3Words, what3Words), - updated_at =:updatedAt + updated_at =:updatedAt, + network_photo_count =COALESCE(:photoCount, network_photo_count) WHERE id=:id AND network_id=:networkId """, ) @@ -276,6 +277,7 @@ interface WorksiteDao { svi: Float?, what3Words: String?, updatedAt: Instant, + photoCount: Int?, ) @Transaction @@ -293,7 +295,8 @@ interface WorksiteDao { plus_code =COALESCE(plus_code, :plusCode), svi =COALESCE(svi, :svi), reported_by =COALESCE(reported_by, :reportedBy), - what3Words =COALESCE(what3Words, :what3Words) + what3Words =COALESCE(what3Words, :what3Words), + network_photo_count =COALESCE(:photoCount, network_photo_count) WHERE id=:id """, ) @@ -310,6 +313,7 @@ interface WorksiteDao { svi: Float?, reportedBy: Long?, what3Words: String?, + photoCount: Int?, ) @Transaction diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt index eac63b935..69173a0d3 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/dao/WorksiteDaoPlus.kt @@ -203,6 +203,7 @@ class WorksiteDaoPlus @Inject constructor( svi = svi, what3Words = what3Words, updatedAt = updatedAt, + photoCount = photoCount, ) } @@ -347,12 +348,11 @@ class WorksiteDaoPlus @Inject constructor( true, ) - val worksiteId = - if (isUpdated) { - db.worksiteDao().getWorksiteId(core.networkId) - } else { - -1 - } + val worksiteId = if (isUpdated) { + db.worksiteDao().getWorksiteId(core.networkId) + } else { + -1 + } return@withTransaction Pair(isUpdated, worksiteId) } @@ -377,6 +377,7 @@ class WorksiteDaoPlus @Inject constructor( svi = svi, reportedBy = reportedBy, what3Words = what3Words, + photoCount = photoCount, ) } diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt index f8051fa38..872849abd 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/PopulatedWorksite.kt @@ -2,6 +2,7 @@ package com.crisiscleanup.core.database.model import androidx.room.ColumnInfo import androidx.room.Embedded +import androidx.room.Junction import androidx.room.Relation import com.crisiscleanup.core.model.data.WorkTypeStatusClaim import com.crisiscleanup.core.model.data.Worksite @@ -117,6 +118,25 @@ data class PopulatedWorksiteMapVisual( entityColumn = "worksite_id", ) val formData: List, + + // Has photo + @ColumnInfo("network_photo_count") + val networkPhotoCount: Int, + @Relation( + parentColumn = "id", + entityColumn = "id", + associateBy = Junction( + value = WorksiteNetworkFileCrossRef::class, + parentColumn = "worksite_id", + entityColumn = "network_file_id", + ), + ) + val fileImages: List, + @Relation( + parentColumn = "id", + entityColumn = "worksite_id", + ) + val localImages: List, ) private val highPriorityFlagLiteral = WorksiteFlagType.HighPriority.literal @@ -136,4 +156,7 @@ fun PopulatedWorksiteMapVisual.asExternalModel(isFilteredOut: Boolean = false) = }, isDuplicate = flags.any { it.reasonT == duplicateFlagLiteral }, isFilteredOut = isFilteredOut, + hasPhotos = networkPhotoCount > 0 || + fileImages.any { !it.isDeleted } || + localImages.isNotEmpty(), ) diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt index 0d74afd08..161fbb702 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/Worksite.kt @@ -43,6 +43,7 @@ fun Worksite.asEntities( what3Words = what3Words ?: "", updatedAt = modifiedAt, isLocalFavorite = isLocalFavorite, + photoCount = null, ) val flagsEntities = flags?.map { flag -> diff --git a/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt b/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt index 91537578f..677b94c40 100644 --- a/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt +++ b/core/database/src/main/java/com/crisiscleanup/core/database/model/WorksiteEntity.kt @@ -131,6 +131,8 @@ data class WorksiteEntity( val what3Words: String?, @ColumnInfo("updated_at") val updatedAt: Instant, + @ColumnInfo("network_photo_count", defaultValue = "0") + val photoCount: Int?, // TODO Write tests throughout (model, data, edit feature) /** diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt index c1e21da14..391ea2225 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/component/Button.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.crisiscleanup.core.designsystem.icon.CrisisCleanupIcons import com.crisiscleanup.core.designsystem.theme.CrisisCleanupTheme +import com.crisiscleanup.core.designsystem.theme.LocalDimensions import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.cancelButtonContainerColor import com.crisiscleanup.core.designsystem.theme.cancelButtonContentColor @@ -126,8 +127,7 @@ fun BusyButton( elevation = if (indicateBusy) null else ButtonDefaults.elevatedButtonElevation(), ) { if (indicateBusy) { - // TODO Common dimensions - CircularProgressIndicator(Modifier.size(24.dp)) + CircularProgressIndicator(Modifier.size(LocalDimensions.current.buttonSpinnerSize)) } else { Text(textResId, text, style) } @@ -141,6 +141,7 @@ fun CrisisCleanupButton( enabled: Boolean = true, @StringRes textResId: Int = 0, text: String = "", + indicateBusy: Boolean = false, colors: ButtonColors = primaryButtonColors(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), ) { @@ -152,7 +153,11 @@ fun CrisisCleanupButton( shape = roundedRectangleButtonShape(), elevation = elevation, ) { - Text(textResId, text) + if (indicateBusy) { + CircularProgressIndicator(Modifier.size(LocalDimensions.current.buttonSpinnerSize)) + } else { + Text(textResId, text) + } } } @@ -204,6 +209,7 @@ fun CrisisCleanupOutlinedButton( text: String = "", onClick: () -> Unit = {}, enabled: Boolean = false, + indicateBusy: Boolean = false, borderColor: Color = LocalContentColor.current, fontWeight: FontWeight? = null, style: TextStyle = LocalFontStyles.current.header4, @@ -221,7 +227,9 @@ fun CrisisCleanupOutlinedButton( shape = roundedRectangleButtonShape(), border = border, ) { - if (buttonText.isNotBlank()) { + if (indicateBusy) { + CircularProgressIndicator(Modifier.size(LocalDimensions.current.buttonSpinnerSize)) + } else if (buttonText.isNotBlank()) { Text( buttonText, fontWeight = fontWeight, @@ -288,6 +296,7 @@ fun CrisisCleanupFab( fun WorkTypeAction( text: String, enabled: Boolean, + indicateBusy: Boolean = false, onClick: () -> Unit = {}, ) = CrisisCleanupOutlinedButton( // TODO Common dimensions @@ -297,12 +306,21 @@ fun WorkTypeAction( text = text, onClick = onClick, enabled = enabled, + indicateBusy = indicateBusy, ) +@Composable +fun WorkTypeBusyAction( + text: String, + enabled: Boolean, + onClick: () -> Unit = {}, +) = WorkTypeAction(text, enabled = enabled, indicateBusy = !enabled, onClick) + @Composable fun WorkTypePrimaryAction( text: String, enabled: Boolean, + indicateBusy: Boolean = false, onClick: () -> Unit = {}, ) = CrisisCleanupButton( // TODO Common dimensions @@ -312,6 +330,7 @@ fun WorkTypePrimaryAction( text = text, onClick = onClick, enabled = enabled, + indicateBusy = indicateBusy, elevation = ButtonDefaults.buttonElevation( defaultElevation = 1.dp, ), diff --git a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt index 060068a2a..e6227d2ef 100644 --- a/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt +++ b/core/designsystem/src/main/java/com/crisiscleanup/core/designsystem/theme/Dimensions.kt @@ -29,6 +29,8 @@ data class Dimensions( val isLandscape: Boolean = false, val isPortrait: Boolean = true, val isListDetailWidth: Boolean = false, + + val buttonSpinnerSize: Dp = 24.dp, ) { val itemInnerSpacingHorizontalFlexible: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(itemInnerPaddingHorizontalFlexible) diff --git a/core/domain/.gitignore b/core/domain/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/core/domain/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts deleted file mode 100644 index 4e723e2bf..000000000 --- a/core/domain/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - alias(libs.plugins.nowinandroid.android.library) - alias(libs.plugins.nowinandroid.android.library.jacoco) -} - -android { - namespace = "com.crisiscleanup.core.domain" -} - -dependencies { - implementation(projects.core.common) - implementation(projects.core.data) - - testImplementation(projects.core.testing) - - implementation(libs.kotlinx.coroutines.android) - implementation(libs.kotlinx.datetime) - - implementation(libs.hilt.android) -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt b/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt deleted file mode 100644 index ded49f55d..000000000 --- a/core/domain/src/main/java/com/crisiscleanup/core/domain/LoadSelectIncidents.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.crisiscleanup.core.domain - -import com.crisiscleanup.core.data.IncidentSelector -import com.crisiscleanup.core.data.repository.AccountDataRepository -import com.crisiscleanup.core.data.repository.IncidentsRepository -import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository -import com.crisiscleanup.core.model.data.EmptyIncident -import com.crisiscleanup.core.model.data.Incident -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -class LoadSelectIncidents( - incidentsRepository: IncidentsRepository, - accountDataRepository: AccountDataRepository, - private val incidentSelector: IncidentSelector, - private val appPreferencesRepository: LocalAppPreferencesRepository, - private val coroutineScope: CoroutineScope, -) { - val data = combine( - incidentsRepository.incidents, - accountDataRepository.accountData, - ::Pair, - ) - .mapLatest { (incidents, accountData) -> - if (accountData.isCrisisCleanupAdmin) { - incidents - } else { - incidents.filter { accountData.approvedIncidents.contains(it.id) } - } - } - .map { incidents -> - var selectedId = incidentSelector.incidentId.first() - if (selectedId == EmptyIncident.id) { - selectedId = appPreferencesRepository.userPreferences.first().selectedIncidentId - } - - // Update incident data or select first if current incident (ID) not found - var incident = incidents.find { it.id == selectedId } ?: EmptyIncident - if (incident == EmptyIncident && incidents.isNotEmpty()) { - incident = incidents[0] - appPreferencesRepository.setSelectedIncident(incident.id) - } - - incidentSelector.setIncident(incident) - - if (incident.id == EmptyIncident.id) { - IncidentsData.Empty - } else { - IncidentsData.Incidents(incidents) - } - }.stateIn( - scope = coroutineScope, - initialValue = IncidentsData.Loading, - started = SharingStarted.WhileSubscribed(3_000), - ) - - fun selectIncident(incident: Incident) { - coroutineScope.launch { - (data.first() as? IncidentsData.Incidents)?.let { incidentsData -> - incidentsData.incidents.find { it.id == incident.id }?.let { matchingIncident -> - persistIncident(matchingIncident) - } - } - } - } - - suspend fun persistIncident(incident: Incident) { - if (incident != EmptyIncident) { - appPreferencesRepository.setSelectedIncident(incident.id) - incidentSelector.setIncident(incident) - } - } -} 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 7ca07fcc1..5130c4b41 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 @@ -85,6 +85,7 @@ class InMemoryDotProvider @Inject constructor( isDuplicate: Boolean, isFilteredOut: Boolean, isVisited: Boolean, + hasPhotos: Boolean, ): BitmapDescriptor? { val cacheKey = DotCacheKey(statusClaim, isDuplicate, isFilteredOut) synchronized(cache) { @@ -103,6 +104,7 @@ class InMemoryDotProvider @Inject constructor( isDuplicate: Boolean, isFilteredOut: Boolean, isVisited: Boolean, + hasPhotos: Boolean, ): Bitmap? { val cacheKey = DotCacheKey(statusClaim, isDuplicate, isFilteredOut) synchronized(cache) { diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt index 3ce6672a6..6732b7628 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/MapCaseIconProvider.kt @@ -21,6 +21,7 @@ interface MapCaseIconProvider { isDuplicate: Boolean = false, isFilteredOut: Boolean = false, isVisited: Boolean = false, + hasPhotos: Boolean = false, ): BitmapDescriptor? fun getIconBitmap( @@ -30,5 +31,6 @@ interface MapCaseIconProvider { isDuplicate: Boolean = false, isFilteredOut: Boolean = false, isVisited: Boolean = false, + hasPhotos: Boolean = false, ): Bitmap? } diff --git a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt index 2e7f631cb..a5c964e49 100644 --- a/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt +++ b/core/mapmarker/src/main/java/com/crisiscleanup/core/mapmarker/WorkTypeIconProvider.kt @@ -11,8 +11,10 @@ import android.graphics.drawable.Drawable import androidx.collection.LruCache import androidx.compose.ui.geometry.Offset import androidx.core.graphics.alpha +import androidx.core.graphics.createBitmap import androidx.core.graphics.get import androidx.core.graphics.red +import androidx.core.graphics.set import com.crisiscleanup.core.common.AndroidResourceProvider import com.crisiscleanup.core.model.data.WorkTypeStatusClaim import com.crisiscleanup.core.model.data.WorkTypeType @@ -83,7 +85,7 @@ class WorkTypeIconProvider @Inject constructor( private val shadowRadius: Int private val shadowColor = (0xFF666666).toInt() - private val bitmapSizeDp = 36f + 2 * shadowRadiusDp + private val bitmapSizeDp = 40f + 2 * shadowRadiusDp private val bitmapSize: Int private var bitmapCenterOffset = Offset(0f, 0f) @@ -93,6 +95,10 @@ class WorkTypeIconProvider @Inject constructor( private val plusDrawable: Drawable private val plusDrawableTransparent: Drawable + private val cameraDrawable: Drawable + private val cameraDrawableTransparent: Drawable + private val cameraDrawableVerticalOffset: Int + init { bitmapSize = resourceProvider.dpToPx(bitmapSizeDp).toInt() val centerOffset = bitmapSizeDp * 0.5f @@ -100,10 +106,18 @@ class WorkTypeIconProvider @Inject constructor( shadowRadius = resourceProvider.dpToPx(shadowRadiusDp).toInt() + val overlayAlpha = (255 * FILTERED_OUT_MARKER_ALPHA).toInt() plusDrawable = resourceProvider.getDrawable(R.drawable.ic_work_type_plus) plusDrawableTransparent = resourceProvider.getDrawable(R.drawable.ic_work_type_plus).also { - it.alpha = (255 * FILTERED_OUT_MARKER_ALPHA).toInt() + it.alpha = overlayAlpha } + + cameraDrawable = resourceProvider.getDrawable(R.drawable.ic_work_type_photos) + cameraDrawableTransparent = + resourceProvider.getDrawable(R.drawable.ic_work_type_photos).also { + it.alpha = overlayAlpha + } + cameraDrawableVerticalOffset = resourceProvider.dpToPx(2f).toInt() } private fun cacheIconBitmap(cacheKey: WorkTypeIconCacheKey): BitmapDescriptor { @@ -125,6 +139,7 @@ class WorkTypeIconProvider @Inject constructor( isDuplicate: Boolean, isFilteredOut: Boolean, isVisited: Boolean, + hasPhotos: Boolean, ): BitmapDescriptor { val cacheKey = WorkTypeIconCacheKey( statusClaim, @@ -135,6 +150,7 @@ class WorkTypeIconProvider @Inject constructor( isDuplicate = isDuplicate, isFilteredOut = isFilteredOut, isVisited = isVisited, + hasPhotos = hasPhotos, ) synchronized(cache) { cache.get(cacheKey)?.let { @@ -152,6 +168,7 @@ class WorkTypeIconProvider @Inject constructor( isDuplicate: Boolean, isFilteredOut: Boolean, isVisited: Boolean, + hasPhotos: Boolean, ): Bitmap? { val cacheKey = WorkTypeIconCacheKey( statusClaim, @@ -160,6 +177,7 @@ class WorkTypeIconProvider @Inject constructor( isDuplicate, isFilteredOut, isVisited, + hasPhotos, ) synchronized(cache) { bitmapCache.get(cacheKey)?.let { @@ -186,11 +204,7 @@ class WorkTypeIconProvider @Inject constructor( } val drawable = resourceProvider.getDrawable(iconResId) - val output = Bitmap.createBitmap( - bitmapSize, - bitmapSize, - Bitmap.Config.ARGB_8888, - ) + val output = createBitmap(bitmapSize, bitmapSize) val canvas = Canvas(output) // TODO Keep bounds squared and icon centered @@ -231,7 +245,7 @@ class WorkTypeIconProvider @Inject constructor( (colorValue and 0x00FF00) shr 8, (colorValue and 0x0000FF), ) - output.setPixel(w, h, color) + output[w, h] = color } } } @@ -241,27 +255,59 @@ class WorkTypeIconProvider @Inject constructor( val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { colorFilter = PorterDuffColorFilter(shadowColor, PorterDuff.Mode.SRC_IN) } - val flatShadow = Bitmap.createBitmap( - bitmapSize, - bitmapSize, - Bitmap.Config.ARGB_8888, - ) + val flatShadow = createBitmap(bitmapSize, bitmapSize) Canvas(flatShadow).apply { drawBitmap(output, Matrix(), paint) } blurred = Toolkit.blur(flatShadow, shadowRadius) } + fun drawOverlay( + transparentDrawable: Drawable, + drawable: Drawable, + isLeftAligned: Boolean, + verticalOffset: Int = 0, + ) { + val pd = if (cacheKey.isFilteredOut) transparentDrawable else drawable + + val horizontalOffsetStart = if (isLeftAligned) { + 0 + } else { + rightBounds - pd.intrinsicWidth + } + val horizontalOffsetEnd = if (isLeftAligned) { + pd.intrinsicWidth + } else { + rightBounds + } + val overlayBottom = bottomBounds - verticalOffset + pd.setBounds( + horizontalOffsetStart, + overlayBottom - pd.intrinsicHeight, + horizontalOffsetEnd, + overlayBottom, + ) + pd.draw(canvas) + } + if (cacheKey.hasMultipleWorkTypes) { synchronized(plusDrawable) { - val pd = if (cacheKey.isFilteredOut) plusDrawableTransparent else plusDrawable - pd.setBounds( - rightBounds - pd.intrinsicWidth, - bottomBounds - pd.intrinsicHeight, - rightBounds, - bottomBounds, + drawOverlay( + transparentDrawable = plusDrawableTransparent, + drawable = plusDrawable, + isLeftAligned = false, + ) + } + } + + if (cacheKey.hasPhotos) { + synchronized(cameraDrawable) { + drawOverlay( + transparentDrawable = cameraDrawableTransparent, + drawable = cameraDrawable, + isLeftAligned = true, + verticalOffset = cameraDrawableVerticalOffset, ) - pd.draw(canvas) } } @@ -330,4 +376,5 @@ private data class WorkTypeIconCacheKey( val isDuplicate: Boolean = false, val isFilteredOut: Boolean = false, val isVisited: Boolean = false, + val hasPhotos: Boolean = false, ) diff --git a/core/mapmarker/src/main/res/drawable/ic_work_type_photos.xml b/core/mapmarker/src/main/res/drawable/ic_work_type_photos.xml new file mode 100644 index 000000000..03a391933 --- /dev/null +++ b/core/mapmarker/src/main/res/drawable/ic_work_type_photos.xml @@ -0,0 +1,14 @@ + + + diff --git a/core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/IncidentsData.kt similarity index 55% rename from core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt rename to core/model/src/main/java/com/crisiscleanup/core/model/data/IncidentsData.kt index f390e5ca7..894da6440 100644 --- a/core/domain/src/main/java/com/crisiscleanup/core/domain/IncidentsData.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/IncidentsData.kt @@ -1,13 +1,14 @@ -package com.crisiscleanup.core.domain - -import com.crisiscleanup.core.model.data.Incident +package com.crisiscleanup.core.model.data sealed interface IncidentsData { data object Loading : IncidentsData data class Incidents( val incidents: List, - ) : IncidentsData + val selected: Incident, + ) : IncidentsData { + val selectedId: Long = selected.id + } data object Empty : IncidentsData } diff --git a/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt b/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt index 0a27b1537..6a4b014b9 100644 --- a/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt +++ b/core/model/src/main/java/com/crisiscleanup/core/model/data/WorksiteMapMark.kt @@ -17,4 +17,5 @@ data class WorksiteMapMark( * FALSE renders the mark as usual. */ val isFilteredOut: Boolean = false, + val hasPhotos: Boolean = false, ) diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkFile.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkFile.kt index 22c36939f..3d66a2be8 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkFile.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkFile.kt @@ -24,7 +24,7 @@ data class NetworkFile( @SerialName("large_thumbnail_url") val largeThumbnailUrl: String? = null, @SerialName("mime_content_type") - val mimeContentType: String, + val mimeContentType: String?, val notes: String? = null, @SerialName("small_thumbnail_url") val smallThumbnailUrl: String? = null, diff --git a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt index 9f61ae957..c6e1381dd 100644 --- a/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt +++ b/core/network/src/main/java/com/crisiscleanup/core/network/model/NetworkWorksite.kt @@ -314,6 +314,8 @@ data class NetworkWorksitePage( val what3words: String? = null, @SerialName("work_types") private val workTypes: List, + @SerialName("photos_count") + val photoCount: Int? = null, ) : WorksiteDataSubset { @Transient var newestWorkTypes: List = emptyList() diff --git a/core/renderscript-toolkit/src/main/java/com/google/android/renderscript/Toolkit.kt b/core/renderscript-toolkit/src/main/java/com/google/android/renderscript/Toolkit.kt index 8de89fbac..36b3b6053 100644 --- a/core/renderscript-toolkit/src/main/java/com/google/android/renderscript/Toolkit.kt +++ b/core/renderscript-toolkit/src/main/java/com/google/android/renderscript/Toolkit.kt @@ -1,6 +1,7 @@ package com.google.android.renderscript import android.graphics.Bitmap +import androidx.core.graphics.createBitmap // This string is used for error messages. private const val EXTERNAL_NAME = "RenderScript Toolkit" @@ -1498,7 +1499,7 @@ internal fun validateBitmap( } internal fun createCompatibleBitmap(inputBitmap: Bitmap) = - Bitmap.createBitmap(inputBitmap.width, inputBitmap.height, inputBitmap.config) + createBitmap(inputBitmap.width, inputBitmap.height, inputBitmap.config!!) internal fun validateHistogramDotCoefficients( coefficients: FloatArray?, diff --git a/core/selectincident/build.gradle.kts b/core/selectincident/build.gradle.kts index bc881c4c2..546919993 100644 --- a/core/selectincident/build.gradle.kts +++ b/core/selectincident/build.gradle.kts @@ -11,7 +11,6 @@ android { dependencies { implementation(projects.core.common) implementation(projects.core.designsystem) - implementation(projects.core.domain) implementation(projects.core.model) api(libs.androidx.compose.foundation) diff --git a/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt index 9a966fbe9..f7b153bc7 100644 --- a/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt +++ b/core/selectincident/src/main/java/com/crisiscleanup/core/selectincident/SelectIncidentDialog.kt @@ -40,8 +40,8 @@ import com.crisiscleanup.core.designsystem.LocalAppTranslator import com.crisiscleanup.core.designsystem.component.CrisisCleanupTextButton import com.crisiscleanup.core.designsystem.theme.LocalFontStyles import com.crisiscleanup.core.designsystem.theme.listItemModifier -import com.crisiscleanup.core.domain.IncidentsData import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.IncidentsData import kotlinx.coroutines.launch @Composable diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/LoginWithPhoneViewModel.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/LoginWithPhoneViewModel.kt index 02418c6b2..ed4a71ea0 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/LoginWithPhoneViewModel.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/LoginWithPhoneViewModel.kt @@ -325,9 +325,10 @@ class LoginWithPhoneViewModel @Inject constructor( } } + val authUserId = selectedAccount.value.userId val accountData = accountDataRepository.accountData.first() val authResult = attemptAuthentication( - selectedUserId, + authUserId, otpId = oneTimePasswordId, accountData, ) diff --git a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt index 6cf414024..6f12f4ae4 100644 --- a/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt +++ b/feature/authentication/src/main/java/com/crisiscleanup/feature/authentication/ui/LoginWithPhoneScreen.kt @@ -181,7 +181,7 @@ private fun LoginWithPhoneScreen( .listItemPadding() .actionHeight() .testTag("phoneLoginRequestPhoneNumber"), - enabled = true, + enabled = phoneNumber.isBlank(), action = viewModel::requestPhoneNumber, ) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseAddFlagViewModel.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseAddFlagViewModel.kt index 7e13dd4df..0ad1b4f11 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseAddFlagViewModel.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CaseAddFlagViewModel.kt @@ -391,7 +391,7 @@ class CaseAddFlagViewModel @Inject constructor( true, ) - incidentSelectManager.incident.value = incidentChange + incidentSelectManager.submitIncidentChange(incidentChange) incidentWorksiteChange.value = ExistingWorksiteIdentifier(selectedIncidentId, startingWorksite.id) } catch (e: Exception) { diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt index f6ba6457a..f0b6998d6 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/CreateEditCaseViewModel.kt @@ -576,7 +576,7 @@ class CreateEditCaseViewModel @Inject constructor( } worksiteProvider.updateIncidentChangeWorksite(copiedWorksite) changeWorksiteIncidentId.value = changeIncident.id - incidentSelector.setIncident(changeIncident) + incidentSelector.submitIncidentChange(changeIncident) } else { worksiteProvider.takeIncidentChanged() @@ -678,7 +678,7 @@ class CreateEditCaseViewModel @Inject constructor( worksiteProvider.setEditedLocation(worksite.coordinates) if (isIncidentChange) { - incidentSelector.setIncident(saveChangeIncident) + incidentSelector.submitIncidentChange(saveChangeIncident) } else { editorSetInstant = null dataLoader.reloadData(worksiteId) diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt index 42e44da45..25f302914 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/navigation/CaseEditorNavigation.kt @@ -207,6 +207,16 @@ fun NavController.rerouteToCaseChange(ids: ExistingWorksiteIdentifier) { navigateToCaseEditor(ids.incidentId, ids.worksiteId) } +fun NavController.rerouteToViewCase(ids: ExistingWorksiteIdentifier) { + popBackStack() + while (currentBackStackEntry?.destination?.route?.startsWith(VIEW_CASE_ROUTE) == true + ) { + popBackStack() + } + + navigateToViewCase(ids.incidentId, ids.worksiteId) +} + fun NavGraphBuilder.caseEditSearchAddressScreen( navController: NavHostController, onBack: () -> Unit, diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingWorkTypeViews.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingWorkTypeViews.kt index c1c87ff65..a521639cd 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingWorkTypeViews.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ExistingWorkTypeViews.kt @@ -142,7 +142,7 @@ private fun WorkTypeSummaryView( } } } else { - WorkTypePrimaryAction(t("actions.claim"), isEditable) { + WorkTypePrimaryAction(t("actions.claim"), enabled = isEditable) { updateWorkType(workType.copy(orgClaim = myOrgId), false) } } diff --git a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt index 57d2b5d34..bf3c0c65b 100644 --- a/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt +++ b/feature/caseeditor/src/main/java/com/crisiscleanup/feature/caseeditor/ui/ViewCaseScreen.kt @@ -78,7 +78,7 @@ import com.crisiscleanup.core.designsystem.component.LinkifyEmailText import com.crisiscleanup.core.designsystem.component.LinkifyLocationText import com.crisiscleanup.core.designsystem.component.LinkifyPhoneText import com.crisiscleanup.core.designsystem.component.TemporaryDialog -import com.crisiscleanup.core.designsystem.component.WorkTypeAction +import com.crisiscleanup.core.designsystem.component.WorkTypeBusyAction import com.crisiscleanup.core.designsystem.component.WorkTypePrimaryAction import com.crisiscleanup.core.designsystem.component.actionEdgeSpace import com.crisiscleanup.core.designsystem.component.fabPlusSpaceHeight @@ -502,7 +502,7 @@ private fun ColumnScope.ExistingCaseContent( 2 -> CaseNotesView(worksite) } } - BusyIndicatorFloatingTopCenter(isLoading) + BusyIndicatorFloatingTopCenter(isLoading && pagerState.currentPage > 0) } val closeKeyboard = rememberCloseKeyboard(pagerState) @@ -831,12 +831,17 @@ private fun LazyListScope.workItems( val t = LocalAppTranslator.current val isEditable = LocalCaseEditor.current.isEditable if (profile.unclaimed.isNotEmpty()) { - WorkTypePrimaryAction(t("actions.claim_all_alt"), isEditable, claimAll) + WorkTypePrimaryAction( + t("actions.claim_all_alt"), + enabled = isEditable, + indicateBusy = !isEditable, + claimAll, + ) } if (profile.releasableCount > 0) { - WorkTypeAction(t("actions.release_all"), isEditable, releaseAll) + WorkTypeBusyAction(t("actions.release_all"), isEditable, releaseAll) } else if (profile.requestableCount > 0) { - WorkTypeAction(t("actions.request_all"), isEditable, requestAll) + WorkTypeBusyAction(t("actions.request_all"), isEditable, requestAll) } } } 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 e1293f79d..5dccc5916 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 @@ -43,7 +43,6 @@ import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.OrganizationsRepository import com.crisiscleanup.core.data.repository.WorksiteChangeRepository import com.crisiscleanup.core.data.repository.WorksitesRepository -import com.crisiscleanup.core.domain.LoadSelectIncidents import com.crisiscleanup.core.mapmarker.IncidentBoundsProvider import com.crisiscleanup.core.mapmarker.MapCaseIconProvider import com.crisiscleanup.core.mapmarker.model.MapViewCameraZoom @@ -128,14 +127,7 @@ class CasesViewModel @Inject constructor( @Logger(CrisisCleanupLoggers.Cases) private val logger: AppLogger, val appEnv: AppEnv, ) : ViewModel(), TrimMemoryListener { - val loadSelectIncidents = LoadSelectIncidents( - incidentsRepository = incidentsRepository, - accountDataRepository = accountDataRepository, - incidentSelector = incidentSelector, - appPreferencesRepository = appPreferencesRepository, - coroutineScope = viewModelScope, - ) - val incidentsData = loadSelectIncidents.data + val incidentsData = incidentSelector.data val enableIncidentSelect = incidentsRepository.isFirstLoad .map(Boolean::not) .stateIn( @@ -452,9 +444,7 @@ class CasesViewModel @Inject constructor( incidentSelector.incidentId .onEach { - tileRefreshedInstant = epochZero - mapTileRenderer.setIncident(it, 0, true) - casesMapTileManager.clearTiles() + redrawDotsOverlay(it, 0, epochZero) } .launchIn(viewModelScope) @@ -681,6 +671,20 @@ class CasesViewModel @Inject constructor( } } + private fun redrawDotsOverlay( + incidentId: Long, + worksitesCount: Int, + timestamp: Instant, + ) { + tileRefreshedInstant = timestamp + mapTileRenderer.setIncident(incidentId, worksitesCount, true) + casesMapTileManager.clearTiles() + } + + private fun redrawDotsOverlay(idCount: IncidentIdWorksiteCount) { + redrawDotsOverlay(idCount.id, idCount.totalCount, Clock.System.now()) + } + private suspend fun refreshTiles( idCount: IncidentIdWorksiteCount, pullStats: IncidentDataPullStats, @@ -694,9 +698,7 @@ class CasesViewModel @Inject constructor( val now = Clock.System.now() if (pullStats.isEnded) { - tileRefreshedInstant = now - mapTileRenderer.setIncident(idCount.id, idCount.totalCount, true) - casesMapTileManager.clearTiles() + redrawDotsOverlay(idCount) return@coroutineScope } @@ -711,9 +713,7 @@ class CasesViewModel @Inject constructor( now - pullStats.startTime > tileClearRefreshInterval && sinceLastRefresh > tileClearRefreshInterval if (refreshTiles) { - tileRefreshedInstant = now - mapTileRenderer.setIncident(idCount.id, idCount.totalCount, true) - casesMapTileManager.clearTiles() + redrawDotsOverlay(idCount) } } diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteMapMark.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteMapMark.kt index b7bffd275..c917e875a 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteMapMark.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteMapMark.kt @@ -36,6 +36,7 @@ fun WorksiteMapMark.asWorksiteGoogleMapMark( isDuplicate = isDuplicate, isFilteredOut = isFilteredOut, isVisited = isVisited, + hasPhotos = hasPhotos, ), mapIconOffset = Offset(0.5f + xOffset, 0.5f + yOffset), isFilteredOut = isFilteredOut, diff --git a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteQueryState.kt b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteQueryState.kt index cd943898b..d52cae20a 100644 --- a/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteQueryState.kt +++ b/feature/cases/src/main/java/com/crisiscleanup/feature/cases/model/WorksiteQueryState.kt @@ -17,22 +17,18 @@ val CoordinateBoundsDefault = CoordinateBounds( data class WorksiteQueryState( val incidentId: Long, - val q: String, val zoom: Float, val coordinateBounds: CoordinateBounds, val isTableView: Boolean, - val isZoomInteractive: Boolean, val tableViewSort: WorksiteSortBy, val filters: CasesFilter, ) val WorksiteQueryStateDefault = WorksiteQueryState( incidentId = EmptyIncident.id, - q = "", zoom = 0f, coordinateBounds = CoordinateBoundsDefault, isTableView = false, - isZoomInteractive = false, tableViewSort = WorksiteSortBy.None, filters = CasesFilter(), ) 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 1c4647853..529998dd4 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 @@ -76,7 +76,6 @@ import com.crisiscleanup.core.designsystem.theme.listItemSpacedByHalf import com.crisiscleanup.core.designsystem.theme.navigationContainerColor import com.crisiscleanup.core.designsystem.theme.primaryBlueColor import com.crisiscleanup.core.designsystem.theme.primaryOrangeColor -import com.crisiscleanup.core.domain.IncidentsData import com.crisiscleanup.core.mapmarker.model.MapViewCameraBounds import com.crisiscleanup.core.mapmarker.model.MapViewCameraBoundsDefault import com.crisiscleanup.core.mapmarker.model.MapViewCameraZoom @@ -85,6 +84,7 @@ import com.crisiscleanup.core.mapmarker.ui.rememberMapProperties import com.crisiscleanup.core.mapmarker.ui.rememberMapUiSettings import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.Incident +import com.crisiscleanup.core.model.data.IncidentsData import com.crisiscleanup.core.model.data.Worksite import com.crisiscleanup.core.model.data.WorksiteMapMark import com.crisiscleanup.core.selectincident.SelectIncidentDialog @@ -240,7 +240,7 @@ internal fun CasesRoute( val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() val setSelected = remember(viewModel) { { incident: Incident -> - viewModel.loadSelectIncidents.selectIncident(incident) + viewModel.incidentSelector.selectIncident(incident) } } SelectIncidentDialog( diff --git a/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ViewListViewModel.kt b/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ViewListViewModel.kt index 889c1cb49..61970bfaa 100644 --- a/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ViewListViewModel.kt +++ b/feature/lists/src/main/kotlin/com/crisiscleanup/feature/crisiscleanuplists/ViewListViewModel.kt @@ -15,11 +15,8 @@ import com.crisiscleanup.core.common.network.Dispatcher import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifierNone -import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.IncidentsRepository import com.crisiscleanup.core.data.repository.ListsRepository -import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository -import com.crisiscleanup.core.domain.LoadSelectIncidents import com.crisiscleanup.core.model.data.CrisisCleanupList import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.EmptyList @@ -42,9 +39,7 @@ class ViewListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, listsRepository: ListsRepository, private val incidentsRepository: IncidentsRepository, - accountDataRepository: AccountDataRepository, private val incidentSelector: IncidentSelector, - appPreferencesRepository: LocalAppPreferencesRepository, private val translator: KeyResourceTranslator, @Logger(CrisisCleanupLoggers.Lists) private val logger: AppLogger, @Dispatcher(CrisisCleanupDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, @@ -53,14 +48,6 @@ class ViewListViewModel @Inject constructor( private val listId = viewListArgs.listId - private val loadSelectIncidents = LoadSelectIncidents( - incidentsRepository = incidentsRepository, - accountDataRepository = accountDataRepository, - incidentSelector = incidentSelector, - appPreferencesRepository = appPreferencesRepository, - coroutineScope = viewModelScope, - ) - val viewState = listsRepository.streamList(listId) .mapLatest { list -> if (list == EmptyList) { @@ -138,8 +125,12 @@ class ViewListViewModel @Inject constructor( isChangingIncident = true viewModelScope.launch(ioDispatcher) { try { - loadSelectIncidents.persistIncident(changeIncident) - openWorksiteId = ExistingWorksiteIdentifier(changeIncident.id, changeWorksite.id) + if (incidentSelector.submitIncidentChange(changeIncident)) { + openWorksiteId = + ExistingWorksiteIdentifier(changeIncident.id, changeWorksite.id) + } else { + // TODO Alert no permission to this Incident + } } finally { isChangingIncident = false } diff --git a/feature/menu/build.gradle.kts b/feature/menu/build.gradle.kts index 96912b235..96eb3fb8f 100644 --- a/feature/menu/build.gradle.kts +++ b/feature/menu/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { implementation(projects.core.appComponent) implementation(projects.core.selectincident) + implementation(projects.sync.work) implementation(libs.accompanist.permissions) } \ No newline at end of file diff --git a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt index 413f9bba7..06e2e11df 100644 --- a/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt +++ b/feature/menu/src/main/java/com/crisiscleanup/feature/menu/MenuViewModel.kt @@ -1,5 +1,6 @@ package com.crisiscleanup.feature.menu +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.crisiscleanup.core.appcomponent.AppTopBarDataProvider @@ -20,6 +21,7 @@ import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.incidentcache.DataDownloadSpeedMonitor import com.crisiscleanup.core.data.repository.AccountDataRefresher import com.crisiscleanup.core.data.repository.AccountDataRepository +import com.crisiscleanup.core.data.repository.AppDataManagementRepository import com.crisiscleanup.core.data.repository.CrisisCleanupAccountDataRepository import com.crisiscleanup.core.data.repository.IncidentCacheRepository import com.crisiscleanup.core.data.repository.IncidentsRepository @@ -27,7 +29,9 @@ import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.SyncLogRepository import com.crisiscleanup.core.model.data.InitialIncidentWorksitesCachePreferences import com.crisiscleanup.core.ui.TutorialViewTracker +import com.crisiscleanup.sync.initializers.scheduleInactiveCheckup import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine @@ -47,6 +51,7 @@ class MenuViewModel @Inject constructor( private val accountDataRefresher: AccountDataRefresher, private val appVersionProvider: AppVersionProvider, private val appPreferencesRepository: LocalAppPreferencesRepository, + private val appDataManagementRepository: AppDataManagementRepository, appSettingsProvider: AppSettingsProvider, dataDownloadSpeedMonitor: DataDownloadSpeedMonitor, private val appEnv: AppEnv, @@ -55,6 +60,7 @@ class MenuViewModel @Inject constructor( val tutorialViewTracker: TutorialViewTracker, private val databaseVersionProvider: DatabaseVersionProvider, translator: KeyResourceTranslator, + @ApplicationContext private val context: Context, @ApplicationScope private val externalScope: CoroutineScope, @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, ) : ViewModel() { @@ -72,11 +78,9 @@ class MenuViewModel @Inject constructor( incidentSelector, translator, accountDataRepository, - appPreferencesRepository, viewModelScope, ) val incidentsData = appTopBarDataProvider.incidentsData - val loadSelectIncidents = appTopBarDataProvider.loadSelectIncidents val isLoadingIncidents = incidentsRepository.isLoading val hotlineIncidents = incidentsRepository.hotlineIncidents @@ -207,6 +211,18 @@ class MenuViewModel @Inject constructor( appPreferencesRepository.setMenuTutorialDone(isDone) } } + + fun checkInactivity() { + if (isNotProduction) { + scheduleInactiveCheckup(context) + } + } + + fun clearAppData() { + if (isNotProduction) { + appDataManagementRepository.clearAppData() + } + } } data class MenuItemVisibility( 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 0878f8fa0..e60ec8e4d 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 @@ -42,6 +42,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -189,7 +190,9 @@ private fun MenuScreen( Column { AppTopBar( incidentDropdownModifier = incidentDropdownModifier, - accountToggleModifier = accountToggleModifier, + accountToggleModifier = accountToggleModifier + .testTag("menuAccountToggle"), + incidentSelectTestTag = "menuIncidentSelect", dataProvider = viewModel.appTopBarDataProvider, openAuthentication = openAuthentication, onOpenIncidents = openIncidentsSelect, @@ -391,6 +394,20 @@ private fun MenuScreen( text = "See sync logs", ) } + + item { + CrisisCleanupTextButton( + onClick = viewModel::checkInactivity, + text = "Check inactivity", + ) + } + + item { + CrisisCleanupTextButton( + onClick = viewModel::clearAppData, + text = "Clear app data", + ) + } } } } @@ -401,7 +418,7 @@ private fun MenuScreen( val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() val setSelected = remember(viewModel) { { incident: Incident -> - viewModel.loadSelectIncidents.selectIncident(incident) + viewModel.incidentSelector.selectIncident(incident) } } SelectIncidentDialog( @@ -660,13 +677,17 @@ private fun TermsPrivacyView( horizontalArrangement = Arrangement.Center, ) { CrisisCleanupTextButton( - Modifier.actionHeight(), + Modifier + .actionHeight() + .testTag("menuTermsAction"), text = t("publicNav.terms"), ) { uriHandler.openUri(termsOfServiceUrl) } CrisisCleanupTextButton( - Modifier.actionHeight(), + Modifier + .actionHeight() + .testTag("menuPrivacyAction"), text = t("nav.privacy"), ) { uriHandler.openUri(privacyPolicyUrl) diff --git a/feature/organizationmanage/build.gradle.kts b/feature/organizationmanage/build.gradle.kts index 12ba9eba1..3616f1b15 100644 --- a/feature/organizationmanage/build.gradle.kts +++ b/feature/organizationmanage/build.gradle.kts @@ -11,6 +11,5 @@ android { dependencies { implementation(projects.core.data) implementation(projects.core.designsystem) - implementation(projects.core.domain) implementation(projects.core.selectincident) } \ No newline at end of file diff --git a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt index 48862064d..0bb4fe297 100644 --- a/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt +++ b/feature/organizationmanage/src/main/java/com/crisiscleanup/feature/organizationmanage/InviteTeammateViewModel.kt @@ -23,14 +23,12 @@ import com.crisiscleanup.core.data.IncidentSelectManager import com.crisiscleanup.core.data.IncidentSelector import com.crisiscleanup.core.data.repository.AccountDataRepository import com.crisiscleanup.core.data.repository.IncidentsRepository -import com.crisiscleanup.core.data.repository.LocalAppPreferencesRepository import com.crisiscleanup.core.data.repository.OrgVolunteerRepository import com.crisiscleanup.core.data.repository.OrganizationsRepository -import com.crisiscleanup.core.domain.IncidentsData -import com.crisiscleanup.core.domain.LoadSelectIncidents import com.crisiscleanup.core.model.data.EmptyIncident import com.crisiscleanup.core.model.data.Incident import com.crisiscleanup.core.model.data.IncidentOrganizationInviteInfo +import com.crisiscleanup.core.model.data.IncidentsData import com.crisiscleanup.core.model.data.JoinOrgInvite import com.crisiscleanup.core.model.data.OrgInviteResult import com.crisiscleanup.core.model.data.OrganizationIdName @@ -47,6 +45,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn @@ -60,7 +59,6 @@ class InviteTeammateViewModel @Inject constructor( accountDataRepository: AccountDataRepository, incidentsRepository: IncidentsRepository, incidentSelector: IncidentSelector, - appPreferencesRepository: LocalAppPreferencesRepository, organizationsRepository: OrganizationsRepository, private val orgVolunteerRepository: OrgVolunteerRepository, private val inputValidator: InputValidator, @@ -88,14 +86,7 @@ class InviteTeammateViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(), ) - private val loadSelectIncidents = LoadSelectIncidents( - incidentsRepository = incidentsRepository, - accountDataRepository = accountDataRepository, - incidentSelector = incidentSelector, - appPreferencesRepository = appPreferencesRepository, - coroutineScope = viewModelScope, - ) - private val incidentsData = loadSelectIncidents.data + private val incidentsData = incidentSelector.data val inviteToAnotherOrg = MutableStateFlow(false) private val affiliateOrganizationIds = MutableStateFlow?>(null) @@ -416,10 +407,9 @@ class InviteTeammateViewModel @Inject constructor( init { incidentsData - .filter { it is IncidentsData.Incidents } - .onEach { + .mapNotNull { (it as? IncidentsData.Incidents)?.incidents } + .onEach { incidents -> withContext(ioDispatcher) { - val incidents = (it as IncidentsData.Incidents).incidents this@InviteTeammateViewModel.incidents.value = incidents incidentLookup.value = incidents.associateBy(Incident::id) } diff --git a/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamsViewModel.kt b/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamsViewModel.kt index da7da81b8..8bcedf62d 100644 --- a/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamsViewModel.kt +++ b/feature/team/src/main/java/com/crisiscleanup/feature/team/TeamsViewModel.kt @@ -64,11 +64,9 @@ class TeamsViewModel @Inject constructor( incidentSelector, translator, accountDataRepository, - appPreferencesRepository, viewModelScope, ) val incidentsData = appTopBarDataProvider.incidentsData - val loadSelectIncidents = appTopBarDataProvider.loadSelectIncidents private val additionalUserProfileLookup = MutableStateFlow>(emptyMap()) val viewState = incidentSelector.incidentId diff --git a/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt b/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt index 3cea3c013..0f7aafa16 100644 --- a/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt +++ b/feature/team/src/main/java/com/crisiscleanup/feature/team/ui/TeamsScreen.kt @@ -100,6 +100,7 @@ private fun TeamsScreen( Box { Column { + // TODO Modifiers and test tag AppTopBar( dataProvider = viewModel.appTopBarDataProvider, openAuthentication = openAuthentication, @@ -226,7 +227,7 @@ private fun TeamsScreen( val selectedIncidentId by viewModel.incidentSelector.incidentId.collectAsStateWithLifecycle() val setSelected = remember(viewModel) { { incident: Incident -> - viewModel.loadSelectIncidents.selectIncident(incident) + viewModel.incidentSelector.selectIncident(incident) } } SelectIncidentDialog( diff --git a/settings.gradle.kts b/settings.gradle.kts index 7059bf431..60a5821a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,7 +36,6 @@ include(":core:datastore") include(":core:datastore-proto") include(":core:datastore-test") include(":core:designsystem") -include(":core:domain") include(":core:mapmarker") include(":core:model") include(":core:network") diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncInitializer.kt b/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncInitializer.kt index fdae8c7b6..d41b508a0 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncInitializer.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncInitializer.kt @@ -22,6 +22,9 @@ object Sync { class SyncInitializer : Initializer { override fun create(context: Context): Sync { scheduleSync(context) + scheduleSyncWorksites(context) + scheduleSyncMedia(context) + scheduleInactiveCheckup(context) return Sync } diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncWorkHelpers.kt b/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncWorkHelpers.kt index fb2b0a89e..781c4305a 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncWorkHelpers.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/initializers/SyncWorkHelpers.kt @@ -5,11 +5,13 @@ import android.app.NotificationManager import android.content.Context import androidx.core.app.NotificationCompat import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.ForegroundInfo import androidx.work.NetworkType import androidx.work.WorkManager import com.crisiscleanup.sync.R +import com.crisiscleanup.sync.workers.InactivityWorker import com.crisiscleanup.sync.workers.SyncMediaWorker import com.crisiscleanup.sync.workers.SyncWorker import com.crisiscleanup.sync.workers.SyncWorksitesWorker @@ -26,6 +28,7 @@ private const val SYNC_NOTIFICATION_CHANNEL_ID = "SyncNotificationChannel" internal const val SYNC_WORK_NAME = "SyncWorkName" internal const val SYNC_MEDIA_WORK_NAME = "SyncMediaWorkName" private const val SYNC_WORKSITES_WORK_NAME = "SyncWorksitesWorkName" +private const val INACTIVE_CHECKUP_NAME = "InactiveCheckupName" @Deprecated("Obsolete") private const val SYNC_WORKSITES_FULL_WORK_NAME = "SyncWorksitesFullWorkName" @@ -60,6 +63,16 @@ fun scheduleSyncWorksites(context: Context) { } } +fun scheduleInactiveCheckup(context: Context) { + WorkManager.getInstance(context).apply { + enqueueUniquePeriodicWork( + INACTIVE_CHECKUP_NAME, + ExistingPeriodicWorkPolicy.KEEP, + InactivityWorker.periodicWork(), + ) + } +} + internal val SyncConstraints get() = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/notification/IncidentDataSyncNotifier.kt b/sync/work/src/main/java/com/crisiscleanup/sync/notification/IncidentDataSyncNotifier.kt index 1fcd0e75f..517c0f4e6 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/notification/IncidentDataSyncNotifier.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/notification/IncidentDataSyncNotifier.kt @@ -58,31 +58,31 @@ internal class IncidentDataSyncNotifier @Inject constructor( if (isOngoing && isSyncing ) { - val title = translator.translate("~~Syncing {incident_name}", 0) + val title = translator.translate("sync.syncing_incident_name", 0) .replace("{incident_name}", incidentName) val text = notificationMessage.ifBlank { var message = if (isIndeterminate) { - translator.translate("~~Saving data...", 0) + translator.translate("sync.saving_data", 0) } else if (pullType == IncidentPullDataType.WorksitesCore) { translator.translate( - "~~Saved {case_count}/{total_case_count} Cases.", + "sync.saved_case_count_of_total_count", 0, ) .replace("{case_count}", "$savedCount") .replace("{total_case_count}", "$dataCount") } else if (pullType == IncidentPullDataType.WorksitesAdditional) { translator.translate( - "~~Saved {case_count}/{total_case_count} offline Cases.", + "sync.saved_case_count_of_total_count_offline", 0, ) .replace("{case_count}", "$savedCount") .replace("{total_case_count}", "$dataCount") } else { - translator.translate("~~Saving more data...", 0) + translator.translate("sync.saving_more_data", 0) } if (currentStep in 1..stepTotal) { message = translator.translate( - "~~({current_step}/{total_step_count}) {message}", + "({current_step}/{total_step_count}) {message}", 0, ) .replace("{current_step}", "$currentStep") @@ -103,7 +103,7 @@ internal class IncidentDataSyncNotifier @Inject constructor( .progress(progress) .addAction( R.drawable.close, - translator.translate("~~Stop syncing", 0), + translator.translate("sync.stop_syncing", 0), stopSyncIntent, ) .setOnlyAlertOnce(true) diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/workers/InactivityWorker.kt b/sync/work/src/main/java/com/crisiscleanup/sync/workers/InactivityWorker.kt new file mode 100644 index 000000000..f238b5140 --- /dev/null +++ b/sync/work/src/main/java/com/crisiscleanup/sync/workers/InactivityWorker.kt @@ -0,0 +1,66 @@ +package com.crisiscleanup.sync.workers + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkerParameters +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.common.network.CrisisCleanupDispatchers.IO +import com.crisiscleanup.core.common.network.Dispatcher +import com.crisiscleanup.core.data.repository.AppDataManagementRepository +import com.crisiscleanup.core.data.repository.LocalAppMetricsRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock +import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.days + +private val clearDuration = 180.days +private const val REPEAT_DURATION_DAYS = 15L + +@HiltWorker +internal class InactivityWorker @AssistedInject constructor( + @Assisted private val appContext: Context, + @Assisted workerParams: WorkerParameters, + appMetricsRepository: LocalAppMetricsRepository, + private val appDataManagementRepository: AppDataManagementRepository, + @Logger(CrisisCleanupLoggers.App) private val logger: AppLogger, + @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher, +) : CoroutineWorker(appContext, workerParams) { + private val appMetrics = appMetricsRepository.metrics + + override suspend fun doWork(): Result = withContext(ioDispatcher) { + val latestAppOpen = appMetrics.first().appOpen.date + val delta = Clock.System.now().minus(latestAppOpen) + when { + delta in clearDuration..999.days -> { + if (appDataManagementRepository.backgroundClearAppData(false)) { + Result.success() + } else { + Result.retry() + } + } + + else -> { + val daysToClear = clearDuration.minus(delta) + logger.logDebug("App will clear in ${daysToClear.inWholeDays} days due to inactivity.") + Result.success() + } + } + } + + companion object { + fun periodicWork() = PeriodicWorkRequestBuilder( + REPEAT_DURATION_DAYS, + TimeUnit.DAYS, + ) + .setInputData(InactivityWorker::class.delegatedData()) + .build() + } +} diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncMediaWorker.kt b/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncMediaWorker.kt index cd772246a..632cbcdc3 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncMediaWorker.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncMediaWorker.kt @@ -34,7 +34,7 @@ internal class SyncMediaWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, workerParams) { override suspend fun getForegroundInfo() = appContext.syncForegroundInfo( SYNC_MEDIA_NOTIFICATION_ID, - text = translator.translate("~~Syncing photos and images", 0), + text = translator.translate("sync.syncing_photos_images", 0), ) override suspend fun doWork() = withContext(ioDispatcher) { diff --git a/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncWorksitesWorker.kt b/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncWorksitesWorker.kt index e20ef2fce..907d9d9d7 100644 --- a/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncWorksitesWorker.kt +++ b/sync/work/src/main/java/com/crisiscleanup/sync/workers/SyncWorksitesWorker.kt @@ -34,7 +34,7 @@ internal class SyncWorksitesWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, workerParams) { override suspend fun getForegroundInfo() = appContext.syncForegroundInfo( SYNC_WORKSITES_NOTIFICATION_ID, - text = translator.translate("~~Syncing Cases", 0), + text = translator.translate("sync.syncing_cases", 0), ) override suspend fun doWork() = withContext(ioDispatcher) {