Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ package com.simprints.face.capture.usecases
import android.content.Context
import androidx.preference.PreferenceManager
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.experimental
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class IsUsingAutoCaptureUseCase @Inject constructor(
@ApplicationContext private val context: Context,
@param:ApplicationContext private val context: Context,
) {
private val preference = PreferenceManager.getDefaultSharedPreferences(context)

operator fun invoke(projectConfiguration: ProjectConfiguration): Boolean {
val isFeatureEnabled = projectConfiguration.experimental().faceAutoCaptureEnabled
val isFeatureEnabled = projectConfiguration.face?.isAutoCapture ?: false
return isFeatureEnabled && isOptionTurnedOnInSettings()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ internal class LiveFeedbackAutoCaptureFragmentViewModelTest {
?.qualityThreshold
} returns QUALITY_THRESHOLD
every { isUsingAutoCapture.invoke(any()) } returns true
coEvery {
configRepository.getProjectConfiguration().custom
} returns mapOf("singleQualityFallbackRequired" to JsonPrimitive(false))

every { timeHelper.now() } returnsMany (0..100L).map { Timestamp(it) }
justRun { previewFrame.recycle() }
val resolveFaceBioSdkUseCase = mockk<ResolveFaceBioSdkUseCase> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.simprints.infra.config.store.models.ProjectConfiguration
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
Expand Down Expand Up @@ -38,8 +37,7 @@ class IsUsingAutoCaptureUseCaseTest {
featureEnabled: Boolean,
preferenceEnabled: Boolean,
) {
coEvery { projectConfiguration.custom } returns
mapOf("faceAutoCaptureEnabled" to JsonPrimitive(featureEnabled))
every { projectConfiguration.face?.isAutoCapture } returns featureEnabled
every { sharedPreferences.getBoolean("preference_enable_face_auto_capture", true) } returns preferenceEnabled
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ internal class SettingsFragment : PreferenceFragmentCompat() {
viewModel.generalConfiguration.observe(viewLifecycleOwner) {
enableFingerprintSettings(it.modalities.contains(Modality.FINGERPRINT))
}
viewModel.experimentalConfiguration.observe(viewLifecycleOwner) {
showFaceAutoCaptureSetting(isVisible = it.faceAutoCaptureEnabled)
viewModel.faceConfiguration.observe(viewLifecycleOwner) {
showFaceAutoCaptureSetting(it?.isAutoCapture == true)
}
Comment thread
luhmirin-s marked this conversation as resolved.
viewModel.languagePreference.observe(viewLifecycleOwner) {
loadSelectedLanguage(it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@ package com.simprints.feature.dashboard.settings
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.simprints.core.livedata.LiveDataEvent
import com.simprints.core.livedata.LiveDataEventWithContent
import com.simprints.core.livedata.send
import com.simprints.infra.config.store.ConfigRepository
import com.simprints.infra.config.store.ConfigSyncCache
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.GeneralConfiguration
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.SettingsPasswordConfig
import com.simprints.infra.config.store.models.experimental
import com.simprints.infra.events.device.DeviceEventTracker
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SETTINGS
import com.simprints.infra.logging.Simber
import com.simprints.infra.sync.SyncOrchestrator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject

Expand All @@ -34,10 +31,9 @@ internal class SettingsViewModel @Inject constructor(
get() = _generalConfiguration
private val _generalConfiguration = MutableLiveData<GeneralConfiguration>()

val experimentalConfiguration = configRepository
.observeProjectConfiguration()
.map(ProjectConfiguration::experimental)
.asLiveData(viewModelScope.coroutineContext)
val faceConfiguration: LiveData<FaceConfiguration?>
get() = _faceConfiguration
private val _faceConfiguration = MutableLiveData<FaceConfiguration?>()

val languagePreference: LiveData<String>
get() = _languagePreference
Expand Down Expand Up @@ -77,12 +73,13 @@ internal class SettingsViewModel @Inject constructor(
}

private fun load() = viewModelScope.launch {
val configuration = configRepository.getProjectConfiguration().general
val configuration = configRepository.getProjectConfiguration()

_sinceConfigLastUpdated.send(configSyncCache.sinceLastUpdateTime())
_languagePreference.postValue(configRepository.getDeviceConfiguration().language)
_generalConfiguration.postValue(configuration)
_settingsLocked.postValue(configuration.settingsPassword)
_generalConfiguration.postValue(configuration.general)
_faceConfiguration.postValue(configuration.face)
_settingsLocked.postValue(configuration.general.settingsPassword)
}

fun unlockSettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import com.simprints.core.domain.common.Modality
import com.simprints.infra.config.store.ConfigRepository
import com.simprints.infra.config.store.ConfigSyncCache
import com.simprints.infra.config.store.models.DeviceConfiguration
import com.simprints.infra.config.store.models.ExperimentalProjectConfiguration
import com.simprints.infra.config.store.models.FaceConfiguration
import com.simprints.infra.config.store.models.GeneralConfiguration
import com.simprints.infra.config.store.models.ProjectConfiguration
import com.simprints.infra.config.store.models.SettingsPasswordConfig
import com.simprints.infra.events.device.DeviceEventTracker
import com.simprints.infra.sync.SyncOrchestrator
Expand All @@ -18,7 +17,6 @@ import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand All @@ -40,6 +38,13 @@ class SettingsViewModelTest {
settingsPassword = SettingsPasswordConfig.Locked("1234"),
)

private val faceConfiguration = FaceConfiguration(
allowedSDKs = listOf(),
isAutoCapture = true,
rankOne = null,
simFace = null,
)

@MockK
private lateinit var configRepository: ConfigRepository

Expand All @@ -59,6 +64,7 @@ class SettingsViewModelTest {
MockKAnnotations.init(this, relaxed = true)

coEvery { configRepository.getProjectConfiguration().general } returns generalConfiguration
coEvery { configRepository.getProjectConfiguration().face } returns faceConfiguration
coEvery { configRepository.getDeviceConfiguration().language } returns LANGUAGE

coEvery { configSyncCache.sinceLastUpdateTime() } returnsMany listOf(
Expand All @@ -71,36 +77,23 @@ class SettingsViewModelTest {
viewModel = SettingsViewModel(configRepository, syncOrchestrator, configSyncCache, deviceEventTracker)
}

@Test
fun `experimentalConfiguration live data should follow the project experimental configuration`() = runTest {
val experimentalConfig1 = mapOf("key1" to JsonPrimitive("value1"))
val experimentalConfig2 = mapOf("key2" to JsonPrimitive("value2"))

coEvery { configRepository.observeProjectConfiguration() } returns flowOf(
mockk<ProjectConfiguration>(relaxed = true) {
every { custom } returns experimentalConfig1
},
mockk<ProjectConfiguration>(relaxed = true) {
every { custom } returns experimentalConfig2
},
)
viewModel = SettingsViewModel(configRepository, syncOrchestrator, configSyncCache, deviceEventTracker)

assertThat(viewModel.experimentalConfiguration.test().valueHistory())
.isEqualTo(
listOf(
ExperimentalProjectConfiguration(experimentalConfig1),
ExperimentalProjectConfiguration(experimentalConfig2),
),
)
}

@Test
fun `should initialize the live data correctly`() {
assertThat(viewModel.generalConfiguration.value).isEqualTo(generalConfiguration)
assertThat(viewModel.languagePreference.value).isEqualTo(LANGUAGE)
assertThat(viewModel.settingsLocked.value).isEqualTo(SettingsPasswordConfig.Locked("1234"))
assertThat(viewModel.sinceConfigLastUpdated.value?.peekContent()).isEqualTo(LAST_UPDATED)
assertThat(viewModel.faceConfiguration.value).isEqualTo(faceConfiguration)
}

@Test
fun `should initialize the live data correctly when no face configured`() {
coEvery { configRepository.getProjectConfiguration().face } returns null

// Recreating to set the new value
viewModel = SettingsViewModel(configRepository, syncOrchestrator, configSyncCache, deviceEventTracker)

assertThat(viewModel.faceConfiguration.value).isNull()
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.simprints.infra.config.store.local.ConfigLocalDataSource
import com.simprints.infra.config.store.local.ConfigLocalDataSourceImpl
import com.simprints.infra.config.store.local.migrations.DeviceConfigSharedPrefsMigration
import com.simprints.infra.config.store.local.migrations.ProjectConfigDevicesScopeBatchMigration
import com.simprints.infra.config.store.local.migrations.ProjectConfigFaceAutoCaptureConfigMigration
import com.simprints.infra.config.store.local.migrations.ProjectConfigFaceBioSdkMigration
import com.simprints.infra.config.store.local.migrations.ProjectConfigFaceEmptyVersionMigration
import com.simprints.infra.config.store.local.migrations.ProjectConfigFaceSdkQualityThresholdMigration
Expand Down Expand Up @@ -82,6 +83,7 @@ object DataStoreModule {
projectConfigSampleUploadMigration: ProjectConfigSampleUploadMigration,
projectConfigDownSyncConfigMigration: ProjectConfigSimprintsSyncConfigMigration,
projectConfigDevicesScopeBatchMigration: ProjectConfigDevicesScopeBatchMigration,
projectConfigFaceAutoCaptureConfigMigration: ProjectConfigFaceAutoCaptureConfigMigration,
): DataStore<ProtoProjectConfiguration> = DataStoreFactory.create(
serializer = ProjectConfigurationSerializer,
produceFile = { appContext.dataStoreFile(PROJECT_CONFIG_DATA_STORE_FILE_NAME) },
Expand All @@ -97,6 +99,7 @@ object DataStoreModule {
projectConfigSampleUploadMigration,
projectConfigDownSyncConfigMigration,
projectConfigDevicesScopeBatchMigration,
projectConfigFaceAutoCaptureConfigMigration,
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.simprints.infra.config.store.local.migrations

import androidx.datastore.core.DataMigration
import com.simprints.infra.config.store.local.models.ProtoProjectConfiguration
import com.simprints.infra.logging.LoggingConstants.CrashReportTag.MIGRATION
import com.simprints.infra.logging.Simber
import com.simprints.infra.serialization.SimJson
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Inject

/**
* Can be removed once all the devices have been updated to 2026.3.0
*/
class ProjectConfigFaceAutoCaptureConfigMigration @Inject constructor() : DataMigration<ProtoProjectConfiguration> {
override suspend fun cleanUp() {
Simber.i("Migration of face auto-capture flag is complete", tag = MIGRATION)
}

override suspend fun shouldMigrate(currentData: ProtoProjectConfiguration) = with(currentData) {
hasFace() && !face.hasIsAutoCapture() && hasCustomJson() && customJson.contains("faceAutoCaptureEnabled")
}

override suspend fun migrate(currentData: ProtoProjectConfiguration): ProtoProjectConfiguration {
Simber.i("Start migration of face auto-capture flag", tag = MIGRATION)

val isAutoCapture = try {
SimJson
.parseToJsonElement(currentData.customJson)
.jsonObject["faceAutoCaptureEnabled"]
?.jsonPrimitive
?.booleanOrNull ?: false
} catch (e: SerializationException) {
Simber.e("Failed to parse custom config", e, tag = MIGRATION)
false
}
Comment thread
luhmirin-s marked this conversation as resolved.

return currentData
.toBuilder()
.setFace(
currentData.face
.toBuilder()
.setIsAutoCapture(isAutoCapture)
.build(),
).build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ internal data class OldProjectConfig(
} else {
FaceConfiguration(
allowedSDKs = listOf(ModalitySdkType.RANK_ONE),
isAutoCapture = false,
rankOne = FaceConfiguration.FaceSdkConfiguration(
nbOfImagesToCapture = faceNbOfFramesCaptured?.toIntOrNull() ?: DEFAULT_FACE_FRAMES_TO_CAPTURE,
qualityThreshold = faceQualityThreshold.toFloat(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.simprints.infra.config.store.models.ModalitySdkType
internal fun FaceConfiguration.toProto(): ProtoFaceConfiguration = ProtoFaceConfiguration
.newBuilder()
.addAllAllowedSdks(allowedSDKs.map { it.toProtoFaceConfiguration() })
.setIsAutoCapture(isAutoCapture)
.also { if (rankOne != null) it.rankOne = rankOne.toProto() }
.also { if (simFace != null) it.simFace = simFace.toProto() }
.build()
Expand All @@ -32,6 +33,7 @@ internal fun FaceConfiguration.ImageSavingStrategy.toProto(): ProtoFaceConfigura

internal fun ProtoFaceConfiguration.toDomain(): FaceConfiguration = FaceConfiguration(
allowedSDKs = allowedSdksList.map { it.toDomain() },
isAutoCapture = isAutoCapture,
rankOne = if (hasRankOne()) rankOne.toDomain() else null,
simFace = if (hasSimFace()) simFace.toDomain() else null,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,6 @@ data class ExperimentalProjectConfiguration(
?.booleanOrNull
.let { it == true }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luhmirin-s Just to note that if a user updates to 2026.3.0 and auto-capture was enabled through the custom configuration, the feature will be temporarily disabled until the app syncs the new configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a migration that checks for the stored custom config json and updates the flag in face config so there should not be an interruption.

val faceAutoCaptureEnabled: Boolean
get() = customConfig
?.get(FACE_AUTO_CAPTURE_ENABLED)
?.jsonPrimitive
?.booleanOrNull
.let { it == true }

val faceAutoCaptureImagingDurationMillis: Long
get() = customConfig
?.get(FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is useful, please keep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex-vt restored

Expand Down Expand Up @@ -182,7 +175,6 @@ data class ExperimentalProjectConfiguration(

companion object {
internal const val DISABLE_SUBJECT_POOL_VALIDATION = "disableSubjectPoolValidation"
internal const val FACE_AUTO_CAPTURE_ENABLED = "faceAutoCaptureEnabled"
internal const val FACE_AUTO_CAPTURE_IMAGING_DURATION_MILLIS = "faceAutoCaptureImagingDurationMillis"

internal const val RECORDS_DB_MIGRATION_FROM_REALM_TO_ROOM_ENABLED = "recordsDbMigrationFromRealmEnabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.simprints.core.domain.common.AgeGroup

data class FaceConfiguration(
val allowedSDKs: List<ModalitySdkType>,
val isAutoCapture: Boolean,
val rankOne: FaceSdkConfiguration?,
val simFace: FaceSdkConfiguration?,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import kotlinx.serialization.Serializable
@Serializable
internal data class ApiFaceConfiguration(
val allowedSDKs: List<BioSdk>,
val isAutoCapture: Boolean = false,
val rankOne: ApiFaceSdkConfiguration? = null,
val simFace: ApiFaceSdkConfiguration? = null,
) {
fun toDomain(): FaceConfiguration = FaceConfiguration(
allowedSDKs = allowedSDKs.map { it.toDomain() },
isAutoCapture = isAutoCapture,
rankOne = rankOne?.toDomain(),
simFace = simFace?.toDomain(),
)
Expand Down
2 changes: 2 additions & 0 deletions infra/config-store/src/main/proto/project_config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ message ProtoFaceConfiguration {
optional ProtoFaceSdkConfiguration rank_one = 6;
optional ProtoFaceSdkConfiguration sim_face = 7;

optional bool is_auto_capture = 8;

enum ProtoBioSdk {
RANK_ONE = 0;
SIM_FACE = 1;
Expand Down
Loading