diff --git a/android/buildSrc/src/main/kotlin/BuildConfigs.kt b/android/buildSrc/src/main/kotlin/BuildConfigs.kt index 34baa2404b7..0c6c45632db 100644 --- a/android/buildSrc/src/main/kotlin/BuildConfigs.kt +++ b/android/buildSrc/src/main/kotlin/BuildConfigs.kt @@ -2,8 +2,8 @@ object BuildConfigs { const val minSdk = 26 const val compileSdk = 35 const val targetSdk = 35 - const val versionCode = 19 - const val versionName = "2.2.2" + const val versionCode = 21 + const val versionName = "2.2.3" const val applicationId = "org.smartregister.opensrp" const val jvmToolchain = 17 const val kotlinCompilerExtensionVersion = "1.5.14" diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt index 5fd1e77b9cf..6fcd84ba248 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/sync/AppSyncWorker.kt @@ -78,34 +78,34 @@ constructor( ) override suspend fun doWork(): Result { - kotlin - .runCatching { - saveSyncStartTimestamp() - setForeground(getForegroundInfo()) - customResourceSyncService.runCustomResourceSync() - } - .onSuccess { - return super.doWork() - } - .onFailure { exception -> - when (exception) { - is HttpException -> { - val response = exception.response() - if (response != null && (400..503).contains(response.code())) { - Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") - } + saveSyncStartTimestamp() + // setForeground can throw ForegroundServiceStartNotAllowedException on Android 12+ when the + // periodic sync fires while the app is in the background. Catch this separately so that a + // failed foreground-service start does not prevent the actual sync from running. + runCatching { setForeground(getForegroundInfo()) } + .onFailure { Timber.w(it, "Could not start foreground service for sync; proceeding anyway") } + + return try { + customResourceSyncService.runCustomResourceSync() + super.doWork() + } catch (exception: Exception) { + when (exception) { + is HttpException -> { + val response = exception.response() + if (response != null && (400..503).contains(response.code())) { + Timber.e("HTTP exception ${response.code()} -> ${response.errorBody()}") } - else -> Timber.e(exception) } - syncListenerManager.emitSyncStatus( - SyncState( - counter = SYNC_COUNTER_1, - currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), - ), - ) - return result() + else -> Timber.e(exception) } - return Result.success() + syncListenerManager.emitSyncStatus( + SyncState( + counter = SYNC_COUNTER_1, + currentSyncJobStatus = CurrentSyncJobStatus.Failed(OffsetDateTime.now()), + ), + ) + result() + } } private fun result(): Result = diff --git a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt index 2ceeda3d5a6..08607f46f1b 100644 --- a/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt +++ b/android/engine/src/main/java/org/smartregister/fhircore/engine/ui/components/register/LoaderDialog.kt @@ -27,10 +27,7 @@ import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -112,65 +109,57 @@ private fun LoaderContent( showBackground: Boolean, showLineSpinIndicator: Boolean, ) { - val openDialog = remember { mutableStateOf(true) } - if (openDialog.value) { - Box(modifier.size(boxWidth, boxHeight)) { - Column( - modifier = modifier.padding(8.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + Box(modifier.size(boxWidth, boxHeight)) { + Column( + modifier = modifier.padding(8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Surface( + modifier = modifier.size(boxWidth, boxHeight), + shape = RoundedCornerShape(8.dp), + color = if (showBackground) Color.Black.copy(alpha = 0.56f) else Color.Transparent, ) { - Surface( - modifier = modifier.size(boxWidth, boxHeight), - shape = RoundedCornerShape(8.dp), - color = if (showBackground) Color.Black.copy(alpha = 0.56f) else Color.Transparent, + Column( + modifier = modifier.padding(8.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Column( - modifier = modifier.padding(8.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (showLineSpinIndicator) { - LineSpinFadeLoaderProgressIndicator( - color = Color.White, - lineLength = 12f, - innerRadius = 16f, - ) - } else { - CircularProgressIndicator( - color = Color.White, - strokeWidth = 3.dp, - modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(progressBarSize), - ) - } + if (showLineSpinIndicator) { + LineSpinFadeLoaderProgressIndicator( + color = Color.White, + lineLength = 12f, + innerRadius = 16f, + ) + } else { + CircularProgressIndicator( + color = Color.White, + strokeWidth = 3.dp, + modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_BAR_TAG).size(progressBarSize), + ) + } - dialogMessage?.let { - Text( - text = it, - color = Color.White, - fontSize = 14.sp, - modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(top = 8.dp), - ) - } + dialogMessage?.let { + Text( + text = it, + color = Color.White, + fontSize = 14.sp, + modifier = modifier.testTag(LOADER_DIALOG_PROGRESS_MSG_TAG).padding(top = 8.dp), + ) + } - if (showPercentageProgress) { - Text( - fontSize = 15.sp, - color = Color.White, - text = "$currentPercentage%", - modifier = modifier.padding(top = 4.dp), - ) - } + if (showPercentageProgress) { + Text( + fontSize = 15.sp, + color = Color.White, + text = "$currentPercentage%", + modifier = modifier.padding(top = 4.dp), + ) } } } } } - SideEffect { - if (currentPercentage >= 100) { - openDialog.value = false - } - } } @PreviewWithBackgroundExcludeGenerated diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index e23bb397110..c0f74691666 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidx-test= "1.6.2" appcompat = "1.7.0" benchmark-junit = "1.3.3" cardview = "1.0.0" -common-utils = "1.0.3-SNAPSHOT" +common-utils = "1.0.5" commons-logging = "1.3.5" compose-activity = "1.9.3" compose-fragment = "1.8.5" @@ -20,18 +20,18 @@ core-ktx = "1.13.1" core-testing = "2.2.0" coverallsGradlePlugin = "2.12.2" cqfFhirCr = "3.8.0" -cqfFhirCql = "3.8.0-OpenSRP-2-SNAPSHOT" +cqfFhirCql = "3.8.0-opensrp-hotfix-SNAPSHOT" dagger-hilt = "2.57.1" datastore = "1.1.2" desugar-jdk-libs = "2.1.5" dokkaBase = "1.9.20" -easyRulesCore = "4.1.1-SNAPSHOT" +easyRulesCore = "4.1.1" espresso-core = "3.6.1" fhir-sdk-common = "0.1.0-alpha05-preview3-SNAPSHOT" fhir-sdk-contrib-barcode = "0.1.0-beta3-preview7-SNAPSHOT" fhir-sdk-contrib-locationwidget = "0.1.0-alpha01-preview2-SNAPSHOT" fhir-sdk-data-capture = "1.3.0-preview1-SNAPSHOT" -fhir-sdk-engine = "1.2.0-preview-SNAPSHOT" +fhir-sdk-engine = "1.2.0-preview1-SNAPSHOT" fhir-sdk-knowledge = "0.1.0-beta01-preview1-SNAPSHOT" fhir-sdk-workflow = "0.1.0-beta01-preview2-SNAPSHOT" generativeai = "0.9.0" @@ -73,7 +73,7 @@ okhttp = "4.12.0" okhttp-logging-interceptor = "4.12.0" orchestrator = "1.5.1" owasp = "8.2.1" -p2p-lib = "0.6.11-SNAPSHOT" +p2p-lib = "0.6.11" playServicesLocation = "21.3.0" playServicesTasks = "18.2.0" preference-ktx = "1.2.1" diff --git a/android/quest/build.gradle.kts b/android/quest/build.gradle.kts index 5a31a9dabd8..2d0f9ea9fc8 100644 --- a/android/quest/build.gradle.kts +++ b/android/quest/build.gradle.kts @@ -1,4 +1,5 @@ import com.android.build.api.variant.FilterConfiguration.FilterType +import com.android.build.gradle.internal.api.BaseVariantOutputImpl import io.sentry.android.gradle.extensions.InstrumentationFeature import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel import java.io.FileReader @@ -273,6 +274,13 @@ android { manifestPlaceholders["appLabel"] = "eCHIS" } + create("echisTraining") { + dimension = "apps" + applicationId = "ug.go.health.echis.training" + versionNameSuffix = "-echisTraining" + manifestPlaceholders["appLabel"] = "Training eCHIS" + } + create("sidBunda") { dimension = "apps" applicationIdSuffix = ".sidBunda" @@ -433,6 +441,16 @@ android { "app_name", "\"${variant.mergedFlavor.manifestPlaceholders["appLabel"]}\"", ) + + variant.outputs.all { + val output = this as BaseVariantOutputImpl + val buildTypeName = variant.buildType.name + output.outputFileName = + output.outputFileName.replace( + "-$buildTypeName.apk", + "-${BuildConfigs.versionName}(${BuildConfigs.versionCode})-$buildTypeName.apk", + ) + } } applicationVariants.all { diff --git a/android/quest/src/echisTraining/res/drawable/ic_app_logo.png b/android/quest/src/echisTraining/res/drawable/ic_app_logo.png new file mode 100644 index 00000000000..87889cef09f Binary files /dev/null and b/android/quest/src/echisTraining/res/drawable/ic_app_logo.png differ diff --git a/android/quest/src/echisTraining/res/drawable/ic_launcher.png b/android/quest/src/echisTraining/res/drawable/ic_launcher.png new file mode 100644 index 00000000000..87889cef09f Binary files /dev/null and b/android/quest/src/echisTraining/res/drawable/ic_launcher.png differ diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt index a2017daf12e..91bfc2e9038 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterScreen.kt @@ -51,6 +51,7 @@ import androidx.navigation.compose.rememberNavController import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +import com.google.android.fhir.sync.CurrentSyncJobStatus import kotlinx.coroutines.flow.flowOf import org.hl7.fhir.r4.model.ResourceType import org.smartregister.fhircore.engine.R @@ -172,7 +173,15 @@ fun RegisterScreen( }, ) { innerPadding -> Box(modifier = modifier.padding(innerPadding)) { - if (!BuildConfig.SKIP_AUTHENTICATION && registerUiState.isFirstTimeSync) { + // Tear down the first-time sync dialog (and its full-screen scrim) on a single authoritative + // signal: the sync reaching a terminal state. Relying on isFirstTimeSync alone left the scrim + // composed until a fragile, async recompute (timestamp write + register count) landed, which + // could lag badly or never complete on low-resource devices, stranding a touch-blocking + // overlay. The terminal CurrentSyncJobStatus is delivered to all listeners on the same event. + val syncTerminated = + appDrawerUIState.currentSyncJobStatus is CurrentSyncJobStatus.Succeeded || + appDrawerUIState.currentSyncJobStatus is CurrentSyncJobStatus.Failed + if (!BuildConfig.SKIP_AUTHENTICATION && registerUiState.isFirstTimeSync && !syncTerminated) { LoaderDialog( modifier = modifier.testTag(FIRST_TIME_SYNC_DIALOG), percentageProgressFlow = flowOf(appDrawerUIState.percentageProgress ?: 0), diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt index 13e0157bc27..0753615d317 100644 --- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt +++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/register/RegisterViewModel.kt @@ -625,6 +625,19 @@ constructor( if (registerId.isNotEmpty()) { val paramsMap: Map = params.toParamDataMap() val currentRegisterConfiguration = retrieveRegisterConfiguration(registerId, paramsMap) + + fun buildRegisterUiState() = + RegisterUiState( + screenTitle = currentRegisterConfiguration.registerTitle ?: screenTitle, + isFirstTimeSync = isFirstTimeSync(), + registerConfiguration = currentRegisterConfiguration, + registerId = registerId, + progressPercentage = _percentageProgress, + isSyncUpload = _isUploadSync, + currentSyncJobStatus = _currentSyncJobStatusFlow, + params = params?.toList() ?: emptyList(), + ) + if (currentRegisterConfiguration.infiniteScroll) { registerData.value = getPagerFlow(currentRegisterConfiguration.id, clearCache) } else { @@ -666,20 +679,12 @@ constructor( ) .toInt(), ) + + registerUiState.value = buildRegisterUiState() } } - registerUiState.value = - RegisterUiState( - screenTitle = currentRegisterConfiguration.registerTitle ?: screenTitle, - isFirstTimeSync = isFirstTimeSync(), - registerConfiguration = currentRegisterConfiguration, - registerId = registerId, - progressPercentage = _percentageProgress, - isSyncUpload = _isUploadSync, - currentSyncJobStatus = _currentSyncJobStatusFlow, - params = params?.toList() ?: emptyList(), - ) + registerUiState.value = buildRegisterUiState() } } diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt index 11b5c8a9dec..197c92e73dd 100644 --- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt +++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/appsetting/AppSettingViewModelTest.kt @@ -446,7 +446,10 @@ class AppSettingViewModelTest : RobolectricTest() { appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any()) } coVerify(exactly = 0) { - appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(any(), any()) + appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId( + any(), + any() + ) } } } @@ -484,7 +487,10 @@ class AppSettingViewModelTest : RobolectricTest() { } coEvery { - appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId(any(), any()) + appSettingViewModel.configurationRegistry.fetchRemoteImplementationGuideByAppId( + any(), + any() + ) } returns null coEvery { appSettingViewModel.configurationRegistry.fetchRemoteCompositionByAppId(any())