From 19eddcf496e6ce244129cee3b2dc4027d586adc6 Mon Sep 17 00:00:00 2001 From: SaiD Date: Thu, 4 Dec 2025 23:57:57 +0530 Subject: [PATCH] camera changes --- ...kotlin-compiler-7349953870880591348.salive | 0 app/build.gradle.kts | 1 + .../java/com/example/livingai/MainActivity.kt | 67 +++++--- .../data/repository/AppDataRepositoryImpl.kt | 60 +++++++ .../java/com/example/livingai/di/AppModule.kt | 47 +++--- .../livingai/domain/model/SettingsData.kt | 4 +- .../domain/repository/AppDataRepository.kt | 11 ++ .../domain/usecases/AppDataUseCases.kt | 8 + .../domain/usecases/GetSettingsUseCase.kt | 10 ++ .../domain/usecases/ReadAppEntryUseCase.kt | 11 ++ .../domain/usecases/SaveAppEntryUseCase.kt | 10 ++ .../domain/usecases/SaveSettingsUseCase.kt | 11 ++ .../livingai/pages/camera/CameraScreen.kt | 151 ++++++++---------- .../livingai/pages/camera/CameraViewModel.kt | 138 ++++++++++------ .../pages/camera/VideoRecordScreen.kt | 43 ++++- .../livingai/pages/home/HomeViewModel.kt | 8 +- .../pages/onboarding/OnBoardingViewModel.kt | 14 +- .../livingai/pages/settings/SettingsScreen.kt | 40 +++++ .../pages/settings/SettingsViewModel.kt | 21 +-- .../com/example/livingai/ui/theme/Color.kt | 3 + .../com/example/livingai/utils/Constants.kt | 2 + .../livingai/utils/DistanceFunctions.kt | 98 ++++++++++++ .../livingai/utils/FillScreenHelper.kt | 68 ++++++++ .../livingai/utils/ScreenDimensions.kt | 6 + .../livingai/utils/ScreenOrientation.kt | 27 +++- .../livingai/utils/SilhouetteManager.kt | 102 ++++++------ ...silhoutte.png => angleview_silhouette.png} | Bin ...back_silhoutte.png => back_silhouette.png} | Bin ...ont_silhoutte.png => front_silhouette.png} | Bin ...left_silhoutte.png => left_silhouette.png} | Bin ...silhoutte.png => leftangle_silhouette.png} | Bin ...ght_silhoutte.png => right_silhouette.png} | Bin ...ilhoutte.png => rightangle_silhouette.png} | Bin app/src/main/res/values/strings.xml | 4 +- gradle/libs.versions.toml | 2 + 35 files changed, 702 insertions(+), 265 deletions(-) delete mode 100644 .kotlin/sessions/kotlin-compiler-7349953870880591348.salive create mode 100644 app/src/main/java/com/example/livingai/data/repository/AppDataRepositoryImpl.kt create mode 100644 app/src/main/java/com/example/livingai/domain/repository/AppDataRepository.kt create mode 100644 app/src/main/java/com/example/livingai/domain/usecases/AppDataUseCases.kt create mode 100644 app/src/main/java/com/example/livingai/domain/usecases/GetSettingsUseCase.kt create mode 100644 app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntryUseCase.kt create mode 100644 app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntryUseCase.kt create mode 100644 app/src/main/java/com/example/livingai/domain/usecases/SaveSettingsUseCase.kt create mode 100644 app/src/main/java/com/example/livingai/utils/DistanceFunctions.kt create mode 100644 app/src/main/java/com/example/livingai/utils/FillScreenHelper.kt create mode 100644 app/src/main/java/com/example/livingai/utils/ScreenDimensions.kt rename app/src/main/res/drawable/{angleview_silhoutte.png => angleview_silhouette.png} (100%) rename app/src/main/res/drawable/{back_silhoutte.png => back_silhouette.png} (100%) rename app/src/main/res/drawable/{front_silhoutte.png => front_silhouette.png} (100%) rename app/src/main/res/drawable/{left_silhoutte.png => left_silhouette.png} (100%) rename app/src/main/res/drawable/{leftangle_silhoutte.png => leftangle_silhouette.png} (100%) rename app/src/main/res/drawable/{right_silhoutte.png => right_silhouette.png} (100%) rename app/src/main/res/drawable/{rightangle_silhoutte.png => rightangle_silhouette.png} (100%) diff --git a/.kotlin/sessions/kotlin-compiler-7349953870880591348.salive b/.kotlin/sessions/kotlin-compiler-7349953870880591348.salive deleted file mode 100644 index e69de29..0000000 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02e9809..6374db0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,6 +45,7 @@ android { dependencies { implementation(libs.androidx.paging.common) implementation(libs.androidx.ui) + implementation(libs.androidx.window) val cameraxVersion = "1.5.0-alpha03" implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.cardview:cardview:1.0.0") diff --git a/app/src/main/java/com/example/livingai/MainActivity.kt b/app/src/main/java/com/example/livingai/MainActivity.kt index a17ba23..cb0e0c7 100644 --- a/app/src/main/java/com/example/livingai/MainActivity.kt +++ b/app/src/main/java/com/example/livingai/MainActivity.kt @@ -3,26 +3,33 @@ package com.example.livingai import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle +import androidx.activity.compose.LocalActivityResultRegistryOwner import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import com.example.livingai.domain.usecases.AppDataUseCases import com.example.livingai.pages.home.HomeViewModel import com.example.livingai.pages.navigation.NavGraph -import com.example.livingai.pages.navigation.Route import com.example.livingai.ui.theme.LivingAITheme +import com.example.livingai.utils.LocaleHelper +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel class MainActivity : ComponentActivity() { private val viewModel by viewModel() - + private val appDataUseCases: AppDataUseCases by inject() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) @@ -32,32 +39,42 @@ class MainActivity : ComponentActivity() { } } - // The splash screen is shown until the start destination is determined. - // If there's a delay, the splash screen will cover it. - setContent { - LivingAITheme { - enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( - lightScrim = Color.Transparent.toArgb(), - darkScrim = Color.Transparent.toArgb() - ), - navigationBarStyle = SystemBarStyle.auto( - lightScrim = Color.Transparent.toArgb(), - darkScrim = Color.Transparent.toArgb() + val settings by appDataUseCases.getSettings().collectAsState(initial = null) + val context = LocalContext.current + + // Update locale and provide it through CompositionLocalProvider + val localizedContext = settings?.let { + LocaleHelper.applyLocale(context, it.language) + } ?: context + + CompositionLocalProvider( + LocalContext provides localizedContext, + LocalActivityResultRegistryOwner provides this + ) { + LivingAITheme { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + lightScrim = Color.Transparent.toArgb(), + darkScrim = Color.Transparent.toArgb() + ), + navigationBarStyle = SystemBarStyle.auto( + lightScrim = Color.Transparent.toArgb(), + darkScrim = Color.Transparent.toArgb() + ) ) - ) - Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { - val startDestination = viewModel.startDestination.value - // Ensure startDestination is not null before rendering NavGraph - if (startDestination != null) { - NavGraph(startDestination = startDestination) - } else { - // Optional: Show a loading indicator if startDestination is null - // for an extended period, though splash screen should handle it. + Box(modifier = Modifier.background(color = MaterialTheme.colorScheme.background)) { + val startDestination = viewModel.startDestination.value + // Ensure startDestination is not null before rendering NavGraph + if (startDestination != null) { + NavGraph(startDestination = startDestination) + } else { + // Optional: Show a loading indicator if startDestination is null + // for an extended period, though splash screen should handle it. + } } } } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/data/repository/AppDataRepositoryImpl.kt b/app/src/main/java/com/example/livingai/data/repository/AppDataRepositoryImpl.kt new file mode 100644 index 0000000..4a5d665 --- /dev/null +++ b/app/src/main/java/com/example/livingai/data/repository/AppDataRepositoryImpl.kt @@ -0,0 +1,60 @@ +package com.example.livingai.data.repository + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import com.example.livingai.domain.model.SettingsData +import com.example.livingai.domain.repository.AppDataRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + +class AppDataRepositoryImpl(private val dataStore: DataStore) : AppDataRepository { + + private object PreferencesKeys { + val APP_ENTRY = booleanPreferencesKey("app_entry") + val LANGUAGE = stringPreferencesKey("language") + val IS_AUTO_CAPTURE_ON = booleanPreferencesKey("is_auto_capture_on") + val JACCARD_THRESHOLD = floatPreferencesKey("jaccard_threshold") + } + + override fun getSettings(): Flow { + return dataStore.data.catch { + if (it is IOException) { + emit(emptyPreferences()) + } else { + throw it + } + }.map { + val language = it[PreferencesKeys.LANGUAGE] ?: "en" + val isAutoCaptureOn = it[PreferencesKeys.IS_AUTO_CAPTURE_ON] ?: false + val jaccardThreshold = it[PreferencesKeys.JACCARD_THRESHOLD] ?: 50f + SettingsData(language, isAutoCaptureOn, jaccardThreshold) + } + } + + override suspend fun saveSettings(settings: SettingsData) { + dataStore.edit { + it[PreferencesKeys.LANGUAGE] = settings.language + it[PreferencesKeys.IS_AUTO_CAPTURE_ON] = settings.isAutoCaptureOn + it[PreferencesKeys.JACCARD_THRESHOLD] = settings.jaccardThreshold + } + } + + override suspend fun saveAppEntry() { + dataStore.edit { settings -> + settings[PreferencesKeys.APP_ENTRY] = true + } + } + + override fun readAppEntry(): Flow { + return dataStore.data.map { preferences -> + preferences[PreferencesKeys.APP_ENTRY] ?: false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/di/AppModule.kt b/app/src/main/java/com/example/livingai/di/AppModule.kt index cf1885c..f16b6a2 100644 --- a/app/src/main/java/com/example/livingai/di/AppModule.kt +++ b/app/src/main/java/com/example/livingai/di/AppModule.kt @@ -4,35 +4,36 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import androidx.window.layout.WindowMetricsCalculator import coil.ImageLoader import coil.decode.SvgDecoder import com.example.livingai.data.local.CSVDataSource -import com.example.livingai.data.manager.LocalUserManagerImpl import com.example.livingai.data.ml.AIModelImpl -import com.example.livingai.data.repository.SettingsRepositoryImpl +import com.example.livingai.data.repository.AppDataRepositoryImpl import com.example.livingai.data.repository.business.AnimalDetailsRepositoryImpl import com.example.livingai.data.repository.business.AnimalProfileRepositoryImpl import com.example.livingai.data.repository.business.AnimalRatingRepositoryImpl import com.example.livingai.data.repository.media.CameraRepositoryImpl import com.example.livingai.data.repository.media.VideoRepositoryImpl -import com.example.livingai.domain.manager.LocalUserManager import com.example.livingai.domain.ml.AIModel +import com.example.livingai.domain.repository.AppDataRepository import com.example.livingai.domain.repository.CameraRepository -import com.example.livingai.domain.repository.SettingsRepository import com.example.livingai.domain.repository.VideoRepository import com.example.livingai.domain.repository.business.AnimalDetailsRepository import com.example.livingai.domain.repository.business.AnimalProfileRepository import com.example.livingai.domain.repository.business.AnimalRatingRepository import com.example.livingai.domain.repository.business.DataSource -import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases +import com.example.livingai.domain.usecases.AppDataUseCases import com.example.livingai.domain.usecases.DeleteAnimalProfile import com.example.livingai.domain.usecases.GetAnimalDetails import com.example.livingai.domain.usecases.GetAnimalProfiles import com.example.livingai.domain.usecases.GetAnimalRatings +import com.example.livingai.domain.usecases.GetSettingsUseCase import com.example.livingai.domain.usecases.ProfileEntry.ProfileEntryUseCase import com.example.livingai.domain.usecases.ProfileListing.ProfileListingUseCase -import com.example.livingai.domain.usecases.ReadAppEntry -import com.example.livingai.domain.usecases.SaveAppEntry +import com.example.livingai.domain.usecases.ReadAppEntryUseCase +import com.example.livingai.domain.usecases.SaveAppEntryUseCase +import com.example.livingai.domain.usecases.SaveSettingsUseCase import com.example.livingai.domain.usecases.SetAnimalDetails import com.example.livingai.domain.usecases.SetAnimalRatings import com.example.livingai.pages.addprofile.AddProfileViewModel @@ -46,6 +47,7 @@ import com.example.livingai.pages.settings.SettingsViewModel import com.example.livingai.utils.Constants import com.example.livingai.utils.CoroutineDispatchers import com.example.livingai.utils.DefaultCoroutineDispatchers +import com.example.livingai.utils.ScreenDimensions import com.example.livingai.utils.SilhouetteManager import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.viewModel @@ -54,13 +56,16 @@ import org.koin.dsl.module private val Context.dataStore: DataStore by preferencesDataStore(name = Constants.USER_SETTINGS) val appModule = module { - single { LocalUserManagerImpl(get()) } single> { androidContext().dataStore } + single { AppDataRepositoryImpl(get()) } + single { - AppEntryUseCases( - readAppEntry = ReadAppEntry(get()), - saveAppEntry = SaveAppEntry(get()) + AppDataUseCases( + getSettings = GetSettingsUseCase(get()), + saveSettings = SaveSettingsUseCase(get()), + readAppEntry = ReadAppEntryUseCase(get()), + saveAppEntry = SaveAppEntryUseCase(get()) ) } @@ -86,11 +91,16 @@ val appModule = module { } // Initialize silhouettes once - single(createdAtStart = true) { + single(createdAtStart = true) { val ctx: Context = androidContext() - // Names expected to be provided as drawable resource names like "front_silhoutte" - val names = listOf("front_silhouette", "side_silhouette", "rear_silhouette") - SilhouetteManager.initialize(ctx, names) + val metrics = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(ctx) + + val bounds = metrics.bounds + val screenWidth = bounds.width() + val screenHeight = bounds.height() + SilhouetteManager.initialize(ctx, screenWidth, screenHeight) + ScreenDimensions(screenWidth, screenHeight) } // ML Model @@ -100,7 +110,6 @@ val appModule = module { single { AnimalProfileRepositoryImpl(get()) } single { AnimalDetailsRepositoryImpl(get()) } single { AnimalRatingRepositoryImpl(get()) } - single { SettingsRepositoryImpl(get()) } single { CameraRepositoryImpl(get(), androidContext()) } single { VideoRepositoryImpl(get()) } @@ -129,13 +138,13 @@ val appModule = module { // ViewModels viewModel { HomeViewModel(get()) } - viewModel { OnBoardingViewModel(get(), get()) } + viewModel { OnBoardingViewModel(get()) } viewModel { (savedStateHandle: androidx.lifecycle.SavedStateHandle?) -> AddProfileViewModel(get(), get(), get(), androidContext(), savedStateHandle) } viewModel { ListingsViewModel(get()) } - viewModel { SettingsViewModel(get(), androidContext()) } + viewModel { SettingsViewModel(get()) } viewModel { RatingViewModel(get(), get(), get(), get()) } - viewModel { CameraViewModel(get(), get()) } + viewModel { CameraViewModel(get(), get(), get(), get()) } viewModel { VideoViewModel() } } diff --git a/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt b/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt index 6ecad9b..0013118 100644 --- a/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt +++ b/app/src/main/java/com/example/livingai/domain/model/SettingsData.kt @@ -5,5 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class SettingsData( val language: String = "en", - val isAutoCaptureOn: Boolean = false + val isAutoCaptureOn: Boolean = false, + val jaccardThreshold: Float = 50f, + val distanceMethod: String = "Jaccard" ) diff --git a/app/src/main/java/com/example/livingai/domain/repository/AppDataRepository.kt b/app/src/main/java/com/example/livingai/domain/repository/AppDataRepository.kt new file mode 100644 index 0000000..01f01ea --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/repository/AppDataRepository.kt @@ -0,0 +1,11 @@ +package com.example.livingai.domain.repository + +import com.example.livingai.domain.model.SettingsData +import kotlinx.coroutines.flow.Flow + +interface AppDataRepository { + fun getSettings(): Flow + suspend fun saveSettings(settings: SettingsData) + suspend fun saveAppEntry() + fun readAppEntry(): Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/AppDataUseCases.kt b/app/src/main/java/com/example/livingai/domain/usecases/AppDataUseCases.kt new file mode 100644 index 0000000..74a1737 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/AppDataUseCases.kt @@ -0,0 +1,8 @@ +package com.example.livingai.domain.usecases + +data class AppDataUseCases( + val getSettings: GetSettingsUseCase, + val saveSettings: SaveSettingsUseCase, + val readAppEntry: ReadAppEntryUseCase, + val saveAppEntry: SaveAppEntryUseCase +) \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/GetSettingsUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/GetSettingsUseCase.kt new file mode 100644 index 0000000..4089414 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/GetSettingsUseCase.kt @@ -0,0 +1,10 @@ +package com.example.livingai.domain.usecases + +import com.example.livingai.domain.repository.AppDataRepository +import javax.inject.Inject + +class GetSettingsUseCase @Inject constructor( + private val appDataRepository: AppDataRepository +) { + operator fun invoke() = appDataRepository.getSettings() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntryUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntryUseCase.kt new file mode 100644 index 0000000..98defbb --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/ReadAppEntryUseCase.kt @@ -0,0 +1,11 @@ +package com.example.livingai.domain.usecases + +import com.example.livingai.domain.repository.AppDataRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class ReadAppEntryUseCase @Inject constructor( + private val appDataRepository: AppDataRepository +) { + operator fun invoke(): Flow = appDataRepository.readAppEntry() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntryUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntryUseCase.kt new file mode 100644 index 0000000..d124398 --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/SaveAppEntryUseCase.kt @@ -0,0 +1,10 @@ +package com.example.livingai.domain.usecases + +import com.example.livingai.domain.repository.AppDataRepository +import javax.inject.Inject + +class SaveAppEntryUseCase @Inject constructor( + private val appDataRepository: AppDataRepository +) { + suspend operator fun invoke() = appDataRepository.saveAppEntry() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/domain/usecases/SaveSettingsUseCase.kt b/app/src/main/java/com/example/livingai/domain/usecases/SaveSettingsUseCase.kt new file mode 100644 index 0000000..077841f --- /dev/null +++ b/app/src/main/java/com/example/livingai/domain/usecases/SaveSettingsUseCase.kt @@ -0,0 +1,11 @@ +package com.example.livingai.domain.usecases + +import com.example.livingai.domain.model.SettingsData +import com.example.livingai.domain.repository.AppDataRepository +import javax.inject.Inject + +class SaveSettingsUseCase @Inject constructor( + private val appDataRepository: AppDataRepository +) { + suspend operator fun invoke(settings: SettingsData) = appDataRepository.saveSettings(settings) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt index 44c9f55..44f30f1 100644 --- a/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt +++ b/app/src/main/java/com/example/livingai/pages/camera/CameraScreen.kt @@ -6,20 +6,15 @@ import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageProxy import androidx.camera.view.LifecycleCameraController import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Camera +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -30,14 +25,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.navigation.NavController +import androidx.core.content.ContextCompat import com.example.livingai.pages.components.CameraPreview import com.example.livingai.pages.components.PermissionWrapper import com.example.livingai.pages.navigation.Route import com.example.livingai.utils.SetScreenOrientation -import com.example.livingai.utils.SilhouetteManager import org.koin.androidx.compose.koinViewModel @Composable @@ -47,12 +40,22 @@ fun CameraScreen( orientation: String? = null, animalId: String ) { - val orientationLock = when (orientation) { - "front", "back" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - else -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + val isLandscape = when (orientation) { + "front", "back" -> false + else -> true + } + + val orientationLock = if (isLandscape) { + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } SetScreenOrientation(orientationLock) + LaunchedEffect(animalId, orientation) { + viewModel.onEvent(CameraEvent.SetContext(animalId, orientation)) + } + PermissionWrapper { val state by viewModel.state.collectAsState() val context = LocalContext.current @@ -62,115 +65,101 @@ fun CameraScreen( setEnabledUseCases(LifecycleCameraController.IMAGE_ANALYSIS or LifecycleCameraController.IMAGE_CAPTURE) } } - - LaunchedEffect(animalId, orientation) { - viewModel.onEvent(CameraEvent.SetContext(animalId, orientation)) + + fun takePhoto() { + val executor = ContextCompat.getMainExecutor(context) + controller.takePicture( + executor, + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + viewModel.onEvent(CameraEvent.ImageCaptured(image)) + } + + override fun onError(exception: ImageCaptureException) { + // Handle error, e.g., log it or show a message + } + } + ) + } + + LaunchedEffect(state.shouldAutoCapture) { + if (state.shouldAutoCapture) { + takePhoto() + viewModel.onEvent(CameraEvent.AutoCaptureTriggered) + } } LaunchedEffect(state.capturedImageUri) { state.capturedImageUri?.let { navController.navigate( Route.ViewImageScreen( - imageUri = it.toString(), + imageUri = it.toString(), shouldAllowRetake = true, showAccept = true, orientation = orientation, animalId = animalId ) ) - viewModel.onEvent(CameraEvent.ClearCapturedImage) // Clear after navigation + viewModel.onEvent(CameraEvent.ClearCapturedImage) } } Scaffold( floatingActionButton = { - if (!state.isAutoCaptureOn) { - FloatingActionButton(onClick = { - val executor = ContextCompat.getMainExecutor(context) - controller.takePicture( - executor, - object : ImageCapture.OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { - viewModel.onEvent(CameraEvent.ImageCaptured(image)) - } - - override fun onError(exception: ImageCaptureException) { - // Handle error - } - } - ) - }) { - Icon(Icons.Default.Camera, contentDescription = "Capture Image") - } + FloatingActionButton(onClick = ::takePhoto) { + Icon(Icons.Default.Camera, contentDescription = "Capture Image") } }, floatingActionButtonPosition = FabPosition.Center ) { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), + modifier = Modifier.fillMaxSize(), ) { - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier.fillMaxSize() + ) { CameraPreview( modifier = Modifier.fillMaxSize(), controller = controller, onFrame = { bitmap, rotation -> - if (orientation != null) { - viewModel.onEvent(CameraEvent.FrameReceived(bitmap, rotation)) - } + viewModel.onEvent(CameraEvent.FrameReceived(bitmap, rotation)) } ) - // Overlay silhouette if orientation provided - if (state.orientation != null) { - val silhouetteName = "${state.orientation}_silhouette" - val bmp = SilhouetteManager.getOriginal(silhouetteName) - if (bmp != null) { - Image( - bitmap = bmp.asImageBitmap(), - contentDescription = "overlay", - modifier = Modifier.matchParentSize(), - contentScale = ContentScale.Crop, - alpha = 0.5f - ) - } - } - - // Overlay Segmentation Mask + // The ML segmentation mask state.segmentationMask?.let { mask -> Image( bitmap = mask.asImageBitmap(), - contentDescription = "segmentation overlay", - modifier = Modifier.matchParentSize(), + contentDescription = "Segmentation Overlay", + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.FillBounds, alpha = 0.5f ) } - } - // Top Bar with Inference Result and Auto Capture Switch - Row( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.TopCenter) - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - if (state.inferenceResult != null) { - Text(text = state.inferenceResult!!, color = androidx.compose.ui.graphics.Color.White) - } else { - Box(modifier = Modifier.weight(1f)) - } - - Row(verticalAlignment = Alignment.CenterVertically) { - Text("Auto Capture", color = androidx.compose.ui.graphics.Color.White) - Switch( - checked = state.isAutoCaptureOn, - onCheckedChange = { viewModel.onEvent(CameraEvent.ToggleAutoCapture) } + state.silhouetteMask?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "Silhouette Overlay", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + alpha = 0.4f ) } + + state.savedMaskBitmap?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "Silhouette Overlay", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit, + alpha = 0.4f + ) + } + } + + if (state.isCapturing) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } } } diff --git a/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt b/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt index 4477a30..7eeebfa 100644 --- a/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt +++ b/app/src/main/java/com/example/livingai/pages/camera/CameraViewModel.kt @@ -6,101 +6,144 @@ import android.net.Uri import androidx.camera.core.ImageProxy import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.livingai.data.ml.AIModelImpl import com.example.livingai.domain.ml.AIModel import com.example.livingai.domain.repository.CameraRepository +import com.example.livingai.domain.usecases.AppDataUseCases +import com.example.livingai.utils.ScreenDimensions import com.example.livingai.utils.SilhouetteManager +import com.example.livingai.utils.calculateDistance +import com.example.livingai.utils.fitImageToCrop import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.roundToInt class CameraViewModel( private val cameraRepository: CameraRepository, - private val aiModel: AIModel + private val aiModel: AIModel, + private val screenDims: ScreenDimensions, + private val appDataUseCases: AppDataUseCases ) : ViewModel() { private val _state = MutableStateFlow(CameraUiState()) val state = _state.asStateFlow() - // mutex to prevent parallel captures - private val captureMutex = Mutex() + private val isProcessingFrame = AtomicBoolean(false) + + init { + viewModelScope.launch { + appDataUseCases.getSettings().collect { settings -> + _state.value = _state.value.copy( + isAutoCaptureEnabled = settings.isAutoCaptureOn, + matchThreshold = settings.jaccardThreshold.roundToInt(), + distanceMethod = settings.distanceMethod + ) + } + } + } fun onEvent(event: CameraEvent) { when (event) { - is CameraEvent.CaptureImage -> captureImage() is CameraEvent.ImageCaptured -> handleImageProxy(event.imageProxy) is CameraEvent.FrameReceived -> handleFrame(event.bitmap, event.rotationDegrees) - is CameraEvent.ToggleAutoCapture -> toggleAutoCapture() is CameraEvent.ClearCapturedImage -> clearCaptured() is CameraEvent.SetContext -> setContext(event.animalId, event.orientation) + is CameraEvent.AutoCaptureTriggered -> { + _state.value = _state.value.copy(shouldAutoCapture = false, isCapturing = true) + } } } private fun setContext(animalId: String, orientation: String?) { - _state.value = _state.value.copy(animalId = animalId, orientation = orientation) - } + val silhouetteMask = orientation?.let { SilhouetteManager.getOriginal(it) } + val savedMask = orientation?.let { SilhouetteManager.getInvertedPurple(it) } - private fun toggleAutoCapture() { - _state.value = _state.value.copy(isAutoCaptureOn = !_state.value.isAutoCaptureOn) + _state.value = _state.value.copy( + animalId = animalId, + orientation = orientation, + silhouetteMask = silhouetteMask, + savedMaskBitmap = savedMask + ) } private fun clearCaptured() { - _state.value = _state.value.copy(capturedImage = null, capturedImageUri = null, segmentationMask = null) - } - - private fun captureImage() { - // UI should handle capture and send ImageCaptured event + _state.value = _state.value.copy( + capturedImageUri = null, + segmentationMask = null + ) } private fun handleImageProxy(proxy: ImageProxy) { - // convert to bitmap and then save via repository viewModelScope.launch { val bitmap = cameraRepository.captureImage(proxy) val animalId = _state.value.animalId ?: "unknown" val uriString = cameraRepository.saveImage(bitmap, animalId, _state.value.orientation) - _state.value = _state.value.copy(capturedImageUri = Uri.parse(uriString)) + _state.value = _state.value.copy( + capturedImageUri = Uri.parse(uriString), + isCapturing = false // Reset capturing flag + ) } } private fun handleFrame(bitmap: Bitmap, rotationDegrees: Int) { - val orientation = _state.value.orientation ?: return + if (_state.value.isCapturing || _state.value.shouldAutoCapture) { + return + } - viewModelScope.launch { - if (captureMutex.tryLock()) { + if (isProcessingFrame.compareAndSet(false, true)) { + viewModelScope.launch { try { val result = aiModel.segmentImage(bitmap) if (result != null) { - val (maskBitmap, maskBool) = result - - // Rotate the mask to match the display orientation + val (maskBitmap, _) = result + val rotatedMask = if (rotationDegrees != 0) { val matrix = Matrix().apply { postRotate(rotationDegrees.toFloat()) } - Bitmap.createBitmap(maskBitmap, 0, 0, maskBitmap.width, maskBitmap.height, matrix, true) + Bitmap.createBitmap( + maskBitmap, + 0, + 0, + maskBitmap.width, + maskBitmap.height, + matrix, + true + ) } else { maskBitmap } - _state.value = _state.value.copy(segmentationMask = rotatedMask) + val output = if(_state.value.orientation == "front" || _state.value.orientation == "back") + fitImageToCrop(rotatedMask, screenDims.screenWidth, screenDims.screenHeight) + else + fitImageToCrop(rotatedMask, screenDims.screenHeight, screenDims.screenWidth) - val savedMask = SilhouetteManager.getBitmask("${orientation}_silhouette") - if (savedMask != null) { - if (maskBool.size == savedMask.size) { - val jaccard = (aiModel as AIModelImpl).jaccardIndex(maskBool, savedMask) - - if (_state.value.isAutoCaptureOn && jaccard > 0.5) { - val animalId = _state.value.animalId ?: "unknown" - val uriString = cameraRepository.saveImage(bitmap, animalId, orientation) - _state.value = _state.value.copy(capturedImageUri = Uri.parse(uriString)) - } - - _state.value = _state.value.copy(inferenceResult = "Jaccard: %.2f".format(jaccard)) + _state.value = _state.value.copy( + segmentationMask = output + ) + + if (_state.value.isAutoCaptureEnabled && + _state.value.savedMaskBitmap != null && + output != null + ) { + val isValidCapture = calculateDistance( + _state.value.distanceMethod, + _state.value.savedMaskBitmap!!, + output, + _state.value.matchThreshold + ) + + if (isValidCapture) { + _state.value = _state.value.copy(shouldAutoCapture = true) } } + } else { + _state.value = _state.value.copy( + segmentationMask = null + ) } } finally { - captureMutex.unlock() + isProcessingFrame.set(false) } } } @@ -110,18 +153,21 @@ class CameraViewModel( data class CameraUiState( val animalId: String? = null, val orientation: String? = null, - val capturedImage: Any? = null, val capturedImageUri: Uri? = null, - val inferenceResult: String? = null, - val isAutoCaptureOn: Boolean = false, - val segmentationMask: Bitmap? = null + val segmentationMask: Bitmap? = null, + val savedMaskBitmap: Bitmap? = null, + val silhouetteMask: Bitmap? = null, + val isCapturing: Boolean = false, + val isAutoCaptureEnabled: Boolean = false, + val matchThreshold: Int = 50, + val distanceMethod: String = "Jaccard", + val shouldAutoCapture: Boolean = false ) sealed class CameraEvent { - object CaptureImage : CameraEvent() data class ImageCaptured(val imageProxy: ImageProxy) : CameraEvent() data class FrameReceived(val bitmap: Bitmap, val rotationDegrees: Int) : CameraEvent() - object ToggleAutoCapture : CameraEvent() object ClearCapturedImage : CameraEvent() data class SetContext(val animalId: String, val orientation: String?) : CameraEvent() + object AutoCaptureTriggered : CameraEvent() } diff --git a/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt b/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt index 9c1b369..ee7b271 100644 --- a/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt +++ b/app/src/main/java/com/example/livingai/pages/camera/VideoRecordScreen.kt @@ -3,6 +3,7 @@ package com.example.livingai.pages.camera import android.Manifest import android.annotation.SuppressLint import android.content.ContentValues +import android.content.pm.ActivityInfo import android.net.Uri import android.provider.MediaStore import androidx.camera.core.CameraSelector @@ -12,14 +13,20 @@ import androidx.camera.video.VideoRecordEvent import androidx.camera.view.CameraController import androidx.camera.view.LifecycleCameraController import androidx.camera.view.video.AudioConfig +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Videocam import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -31,12 +38,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.navigation.NavController import com.example.livingai.pages.components.CameraPreview import com.example.livingai.pages.components.PermissionWrapper import com.example.livingai.pages.navigation.Route +import com.example.livingai.ui.theme.RecordingRed +import com.example.livingai.utils.SetScreenOrientation import org.koin.androidx.compose.koinViewModel import java.io.File @@ -47,6 +58,8 @@ fun VideoRecordScreen( navController: NavController, animalId: String ) { + SetScreenOrientation(orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) + val context = LocalContext.current val state by viewModel.state.collectAsState() var recording by remember { mutableStateOf(null) } @@ -116,7 +129,8 @@ fun VideoRecordScreen( } } } - } + }, + containerColor = if (state.isRecording) RecordingRed else FloatingActionButtonDefaults.containerColor ) { Icon( imageVector = if (state.isRecording) Icons.Default.Stop else Icons.Default.Videocam, @@ -127,9 +141,7 @@ fun VideoRecordScreen( floatingActionButtonPosition = FabPosition.Center ) { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CameraPreview( @@ -137,6 +149,29 @@ fun VideoRecordScreen( controller = controller, onFrame = { _, _ -> } ) + + // Overlay + Column( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.5f)) + ) + Spacer( + modifier = Modifier + .height(300.dp) // Constant height for the transparent area + .fillMaxWidth() + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.5f)) + ) + } } } } diff --git a/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt b/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt index f065727..5c384ba 100644 --- a/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt +++ b/app/src/main/java/com/example/livingai/pages/home/HomeViewModel.kt @@ -4,14 +4,14 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases +import com.example.livingai.domain.usecases.AppDataUseCases import com.example.livingai.pages.navigation.Route import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach class HomeViewModel( - private val appEntryUseCases: AppEntryUseCases + private val appDataUseCases: AppDataUseCases ): ViewModel() { private val _splashCondition = mutableStateOf(true) val splashCondition: State = _splashCondition @@ -20,7 +20,7 @@ class HomeViewModel( val startDestination: State = _startDestination init { - appEntryUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> + appDataUseCases.readAppEntry().onEach { shouldStartFromHomeScreen -> if(shouldStartFromHomeScreen){ _startDestination.value = Route.HomeNavigation }else{ @@ -30,4 +30,4 @@ class HomeViewModel( _splashCondition.value = false }.launchIn(viewModelScope) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt index 10a1a79..1bc20da 100644 --- a/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt +++ b/app/src/main/java/com/example/livingai/pages/onboarding/OnBoardingViewModel.kt @@ -2,14 +2,12 @@ package com.example.livingai.pages.onboarding import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.livingai.data.local.model.SettingsData -import com.example.livingai.domain.repository.SettingsRepository -import com.example.livingai.domain.usecases.AppEntry.AppEntryUseCases +import com.example.livingai.domain.model.SettingsData +import com.example.livingai.domain.usecases.AppDataUseCases import kotlinx.coroutines.launch class OnBoardingViewModel( - private val appEntryUseCases: AppEntryUseCases, - private val settingsRepository: SettingsRepository + private val appDataUseCases: AppDataUseCases ): ViewModel() { fun onEvent(event: OnBoardingEvents) { when(event) { @@ -24,7 +22,7 @@ class OnBoardingViewModel( private fun saveAppEntry() { viewModelScope.launch { - appEntryUseCases.saveAppEntry() + appDataUseCases.saveAppEntry() } } @@ -32,7 +30,7 @@ class OnBoardingViewModel( viewModelScope.launch { // Preserving default for other settings or we could fetch existing if needed // For onboarding, assuming defaults for others is fine. - settingsRepository.saveSettings(SettingsData(language = language, isAutoCaptureOn = false)) + appDataUseCases.saveSettings(SettingsData(language = language, isAutoCaptureOn = false)) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt b/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt index de5edd0..d02b99e 100644 --- a/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt +++ b/app/src/main/java/com/example/livingai/pages/settings/SettingsScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,6 +26,7 @@ import com.example.livingai.pages.commons.Dimentions import com.example.livingai.pages.components.CommonScaffold import com.example.livingai.pages.components.LabeledDropdown import org.koin.androidx.compose.koinViewModel +import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -41,6 +44,8 @@ fun SettingsScreen( // Find the display name for the currently selected language value val selectedLanguageEntry = languageMap.entries.find { it.value == settings.language }?.key ?: languageEntries.first() + val distanceMethods = listOf("Jaccard", "Euclidean", "Hamming") + CommonScaffold( navController = navController, title = stringResource(id = R.string.top_bar_settings) @@ -80,6 +85,41 @@ fun SettingsScreen( } ) } + + // Jaccard Threshold Slider and Distance Method + if (settings.isAutoCaptureOn) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = Dimentions.SMALL_PADDING_TEXT) + ) { + LabeledDropdown( + labelRes = R.string.distance_method, + options = distanceMethods, + selected = settings.distanceMethod, + onSelected = { selectedMethod -> + viewModel.saveSettings(settings.copy(distanceMethod = selectedMethod)) + }, + modifier = Modifier.padding(vertical = Dimentions.SMALL_PADDING_TEXT) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = "Match Threshold") + Text(text = "${settings.jaccardThreshold.roundToInt()}%") + } + Slider( + value = settings.jaccardThreshold, + onValueChange = { newValue -> + viewModel.saveSettings(settings.copy(jaccardThreshold = newValue)) + }, + valueRange = 1f..100f, + steps = 99 + ) + } + } } } } diff --git a/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt b/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt index 5474798..1db4f80 100644 --- a/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/example/livingai/pages/settings/SettingsViewModel.kt @@ -1,11 +1,9 @@ package com.example.livingai.pages.settings -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.example.livingai.data.local.model.SettingsData -import com.example.livingai.domain.repository.SettingsRepository -import com.example.livingai.utils.LocaleHelper +import com.example.livingai.domain.model.SettingsData +import com.example.livingai.domain.usecases.AppDataUseCases import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn @@ -13,8 +11,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch class SettingsViewModel( - private val settingsRepository: SettingsRepository, - private val appContext: Context + private val appDataUseCases: AppDataUseCases ) : ViewModel() { private val _settings = MutableStateFlow(SettingsData("en", false)) @@ -22,23 +19,17 @@ class SettingsViewModel( init { getSettings() - // apply locale initially - settingsRepository.getSettings().onEach { - LocaleHelper.applyLocale(appContext, it.language) - }.launchIn(viewModelScope) } private fun getSettings() { - settingsRepository.getSettings().onEach { + appDataUseCases.getSettings().onEach { _settings.value = it }.launchIn(viewModelScope) } fun saveSettings(settings: SettingsData) { viewModelScope.launch { - settingsRepository.saveSettings(settings) - // apply locale immediately - LocaleHelper.applyLocale(appContext, settings.language) + appDataUseCases.saveSettings(settings) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/ui/theme/Color.kt b/app/src/main/java/com/example/livingai/ui/theme/Color.kt index 96b396d..8971ef8 100644 --- a/app/src/main/java/com/example/livingai/ui/theme/Color.kt +++ b/app/src/main/java/com/example/livingai/ui/theme/Color.kt @@ -88,3 +88,6 @@ val alternate_container_contentCol = md_theme_light_onTertiaryContainer val primary_backgroundCol = md_theme_light_background val secondary_backgroundCol = md_theme_light_surface val alternate_backgroundCol = md_theme_light_surfaceVariant + +// Video Recording +val RecordingRed = Color(0xFFFF0000) diff --git a/app/src/main/java/com/example/livingai/utils/Constants.kt b/app/src/main/java/com/example/livingai/utils/Constants.kt index c4327de..a2d2e9c 100644 --- a/app/src/main/java/com/example/livingai/utils/Constants.kt +++ b/app/src/main/java/com/example/livingai/utils/Constants.kt @@ -14,4 +14,6 @@ object Constants { "rightangle", "angleview" ) + + const val JACCARD_THRESHOLD = 85 } \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/DistanceFunctions.kt b/app/src/main/java/com/example/livingai/utils/DistanceFunctions.kt new file mode 100644 index 0000000..6c4ecf0 --- /dev/null +++ b/app/src/main/java/com/example/livingai/utils/DistanceFunctions.kt @@ -0,0 +1,98 @@ +package com.example.livingai.utils + +import android.graphics.Bitmap +import kotlin.math.sqrt + +fun calculateHammingDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean { + if (mask1.width != mask2.width || mask1.height != mask2.height) return false + + val width = mask1.width + val height = mask1.height + val p1 = IntArray(width * height) + val p2 = IntArray(width * height) + + mask1.getPixels(p1, 0, width, 0, 0, width, height) + mask2.getPixels(p2, 0, width, 0, 0, width, height) + + var distance = 0 + + for (i in p1.indices) { + val a = (p1[i] ushr 24) > 0 + val b = (p2[i] ushr 24) > 0 + if (a != b) distance++ + } + + val total = width * height + val allowed = total * (100 - thresholdPercent) / 100 + + return distance <= allowed +} + +fun calculateEuclideanDistance(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean { + if (mask1.width != mask2.width || mask1.height != mask2.height) return false + + val width = mask1.width + val height = mask1.height + val p1 = IntArray(width * height) + val p2 = IntArray(width * height) + + mask1.getPixels(p1, 0, width, 0, 0, width, height) + mask2.getPixels(p2, 0, width, 0, 0, width, height) + + var sum = 0L + for (i in p1.indices) { + val v1 = if ((p1[i] ushr 24) > 0) 255 else 0 + val v2 = if ((p2[i] ushr 24) > 0) 255 else 0 + val diff = v1 - v2 + sum += diff * diff + } + + val dist = sqrt(sum.toDouble()) + val max = sqrt((width * height).toDouble()) * 255.0 + val allowed = max * (100 - thresholdPercent) / 100.0 + + return dist <= allowed +} + +fun calculateJaccardSimilarity(mask1: Bitmap, mask2: Bitmap, thresholdPercent: Int): Boolean { + if (mask1.width != mask2.width || mask1.height != mask2.height) return false + + val width = mask1.width + val height = mask1.height + val p1 = IntArray(width * height) + val p2 = IntArray(width * height) + + mask1.getPixels(p1, 0, width, 0, 0, width, height) + mask2.getPixels(p2, 0, width, 0, 0, width, height) + + var intersection = 0 + var union = 0 + + for (i in p1.indices) { + val a = ((p1[i] ushr 24) and 0xFF) > 0 + val b = ((p2[i] ushr 24) and 0xFF) > 0 + + if (a && b) intersection++ + if (a || b) union++ + } + + if (union == 0) return false + + val score = (intersection.toDouble() / union.toDouble()) * 100.0 + + return score >= thresholdPercent +} + +fun calculateDistance( + method: String, + mask1: Bitmap, + mask2: Bitmap, + thresholdPercent: Int +): Boolean { + return when (method) { + "Hamming" -> calculateHammingDistance(mask1, mask2, thresholdPercent) + "Euclidean" -> calculateEuclideanDistance(mask1, mask2, thresholdPercent) + "Jaccard" -> calculateJaccardSimilarity(mask1, mask2, thresholdPercent) + else -> false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/FillScreenHelper.kt b/app/src/main/java/com/example/livingai/utils/FillScreenHelper.kt new file mode 100644 index 0000000..5ce8d27 --- /dev/null +++ b/app/src/main/java/com/example/livingai/utils/FillScreenHelper.kt @@ -0,0 +1,68 @@ +package com.example.livingai.utils + +import android.graphics.Bitmap +import android.graphics.Canvas + + +fun fitCenterToScreen( + bitmap: Bitmap, + screenW: Int, + screenH: Int +): Bitmap { + + val srcW = bitmap.width + val srcH = bitmap.height + + if (srcW == 0 || srcH == 0) return bitmap + + val scale = minOf( + screenW.toFloat() / srcW, + screenH.toFloat() / srcH + ) + + val newW = (srcW * scale).toInt() + val newH = (srcH * scale).toInt() + + val scaled = Bitmap.createScaledBitmap(bitmap, newW, newH, true) + + val output = Bitmap.createBitmap(screenW, screenH, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + + val left = (screenW - newW) / 2f + val top = (screenH - newH) / 2f + + canvas.drawBitmap(scaled, left, top, null) + + return output +} + +fun fitImageToCrop( + bitmap: Bitmap, + screenW: Int, + screenH: Int +): Bitmap { + val srcW = bitmap.width + val srcH = bitmap.height + + if (srcW == 0 || srcH == 0) return bitmap + + val scale = maxOf( + screenW.toFloat() / srcW, + screenH.toFloat() / srcH + ) + + val newW = (srcW * scale).toInt() + val newH = (srcH * scale).toInt() + + val scaled = Bitmap.createScaledBitmap(bitmap, newW, newH, true) + + val output = Bitmap.createBitmap(screenW, screenH, Bitmap.Config.ARGB_8888) + val canvas = Canvas(output) + + val left = (screenW - newW) / 2f + val top = (screenH - newH) / 2f + + canvas.drawBitmap(scaled, left, top, null) + + return output +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/ScreenDimensions.kt b/app/src/main/java/com/example/livingai/utils/ScreenDimensions.kt new file mode 100644 index 0000000..70c86b2 --- /dev/null +++ b/app/src/main/java/com/example/livingai/utils/ScreenDimensions.kt @@ -0,0 +1,6 @@ +package com.example.livingai.utils + +data class ScreenDimensions( + val screenWidth: Int, + val screenHeight: Int +) \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/ScreenOrientation.kt b/app/src/main/java/com/example/livingai/utils/ScreenOrientation.kt index 371e891..589c9fd 100644 --- a/app/src/main/java/com/example/livingai/utils/ScreenOrientation.kt +++ b/app/src/main/java/com/example/livingai/utils/ScreenOrientation.kt @@ -1,6 +1,8 @@ package com.example.livingai.utils import android.app.Activity +import android.content.Context +import android.content.ContextWrapper import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.platform.LocalContext @@ -9,12 +11,25 @@ import androidx.compose.ui.platform.LocalContext fun SetScreenOrientation(orientation: Int) { val context = LocalContext.current DisposableEffect(Unit) { - val activity = context as Activity - val originalOrientation = activity.requestedOrientation - activity.requestedOrientation = orientation - onDispose { - // restore original orientation when view disappears - activity.requestedOrientation = originalOrientation + val activity = context.findActivity() + if (activity != null) { + val originalOrientation = activity.requestedOrientation + activity.requestedOrientation = orientation + onDispose { + // restore original orientation when view disappears + activity.requestedOrientation = originalOrientation + } + } else { + onDispose { } } } } + +fun Context.findActivity(): Activity? { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/example/livingai/utils/SilhouetteManager.kt b/app/src/main/java/com/example/livingai/utils/SilhouetteManager.kt index edf049f..9d7e0d4 100644 --- a/app/src/main/java/com/example/livingai/utils/SilhouetteManager.kt +++ b/app/src/main/java/com/example/livingai/utils/SilhouetteManager.kt @@ -5,73 +5,65 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color -import android.graphics.Paint -import androidx.annotation.DrawableRes -import androidx.core.graphics.createBitmap +import android.util.Log +import com.example.livingai.R import java.util.concurrent.ConcurrentHashMap object SilhouetteManager { private val originals = ConcurrentHashMap() - private val inverted = ConcurrentHashMap() - private val bitmasks = ConcurrentHashMap() + private val invertedPurple = ConcurrentHashMap() - fun initialize(context: Context, names: List) { - names.forEach { name -> - val resId = context.resources.getIdentifier(name, "drawable", context.packageName) - if (resId != 0) { - val bmp = BitmapFactory.decodeResource(context.resources, resId) - originals[name] = bmp - val inv = invertBitmap(bmp) - inverted[name] = inv - bitmasks[name] = createBitmask(inv) - } + fun initialize(context: Context, width: Int, height: Int) { + val resources = context.resources + val silhouetteList = mapOf( + "front" to R.drawable.front_silhouette, + "back" to R.drawable.back_silhouette, + "left" to R.drawable.left_silhouette, + "right" to R.drawable.right_silhouette, + "leftangle" to R.drawable.leftangle_silhouette, + "rightangle" to R.drawable.rightangle_silhouette, + "angleview" to R.drawable.angleview_silhouette + ) + + silhouetteList.entries.toList().forEach { (name, resId) -> + val bmp = BitmapFactory.decodeResource(resources, resId) + originals[name] = bmp + Log.d("Silhouette", "Dims: ${width} x ${height}") + if (name == "front" || name == "back") + invertedPurple[name] = createInvertedPurpleBitmap(bmp, width, height) + else + invertedPurple[name] = createInvertedPurpleBitmap(bmp, height, width) + + Log.d("Silhouette", "Dims Mask: ${invertedPurple[name]?.width} x ${invertedPurple[name]?.height}") } } + fun getOriginal(name: String): Bitmap? = originals[name] - fun getInverted(name: String): Bitmap? = inverted[name] - fun getBitmask(name: String): BooleanArray? = bitmasks[name] + fun getInvertedPurple(name: String): Bitmap? = invertedPurple[name] - private fun invertBitmap(src: Bitmap): Bitmap { - val bmOut = Bitmap.createBitmap(src.width, src.height, src.config ?: Bitmap.Config.ARGB_8888) - val canvas = Canvas(bmOut) - val paint = Paint() - val colorMatrix = android.graphics.ColorMatrix( - floatArrayOf( - -1f, 0f, 0f, 0f, 255f, - 0f, -1f, 0f, 0f, 255f, - 0f, 0f, -1f, 0f, 255f, - 0f, 0f, 0f, 1f, 0f - ) - ) - paint.colorFilter = android.graphics.ColorMatrixColorFilter(colorMatrix) - canvas.drawBitmap(src, 0f, 0f, paint) - return bmOut - } + private fun createInvertedPurpleBitmap( + src: Bitmap, + targetWidth: Int, + targetHeight: Int + ): Bitmap { + + val width = src.width + val height = src.height + + val pixels = IntArray(width * height) + src.getPixels(pixels, 0, width, 0, 0, width, height) + + val purple = Color.argb(255, 128, 0, 128) - private fun createBitmask(bitmap: Bitmap): BooleanArray { - val w = bitmap.width - val h = bitmap.height - val mask = BooleanArray(w * h) - val pixels = IntArray(w * h) - bitmap.getPixels(pixels, 0, w, 0, 0, w, h) for (i in pixels.indices) { - val c = pixels[i] - val alpha = Color.alpha(c) - // simple threshold: non-transparent and not near-white (assuming inverted is black on transparent/white) - // The inverted logic makes black -> white, white -> black. - // Wait, if original is black silhouette on transparent: - // Inverted: black becomes white, transparent becomes... white? - // Let's check invertBitmap logic. - // Matrix: R' = 255 - R, G' = 255 - G, B' = 255 - B, A' = A. - // If original is Black (0,0,0,255) -> White (255,255,255,255). - // If original is Transparent (0,0,0,0) -> Transparent (0,0,0,0). - - // So "Inverted" means we have White silhouette on Transparent. - // We want bitmask where the silhouette is. - // So we look for non-transparent pixels. - mask[i] = alpha > 16 + val alpha = pixels[i] ushr 24 + pixels[i] = if (alpha == 0) purple else 0x00000000 } - return mask + + val inverted = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) + + return fitCenterToScreen(inverted, targetWidth, targetHeight) } + } diff --git a/app/src/main/res/drawable/angleview_silhoutte.png b/app/src/main/res/drawable/angleview_silhouette.png similarity index 100% rename from app/src/main/res/drawable/angleview_silhoutte.png rename to app/src/main/res/drawable/angleview_silhouette.png diff --git a/app/src/main/res/drawable/back_silhoutte.png b/app/src/main/res/drawable/back_silhouette.png similarity index 100% rename from app/src/main/res/drawable/back_silhoutte.png rename to app/src/main/res/drawable/back_silhouette.png diff --git a/app/src/main/res/drawable/front_silhoutte.png b/app/src/main/res/drawable/front_silhouette.png similarity index 100% rename from app/src/main/res/drawable/front_silhoutte.png rename to app/src/main/res/drawable/front_silhouette.png diff --git a/app/src/main/res/drawable/left_silhoutte.png b/app/src/main/res/drawable/left_silhouette.png similarity index 100% rename from app/src/main/res/drawable/left_silhoutte.png rename to app/src/main/res/drawable/left_silhouette.png diff --git a/app/src/main/res/drawable/leftangle_silhoutte.png b/app/src/main/res/drawable/leftangle_silhouette.png similarity index 100% rename from app/src/main/res/drawable/leftangle_silhoutte.png rename to app/src/main/res/drawable/leftangle_silhouette.png diff --git a/app/src/main/res/drawable/right_silhoutte.png b/app/src/main/res/drawable/right_silhouette.png similarity index 100% rename from app/src/main/res/drawable/right_silhoutte.png rename to app/src/main/res/drawable/right_silhouette.png diff --git a/app/src/main/res/drawable/rightangle_silhoutte.png b/app/src/main/res/drawable/rightangle_silhouette.png similarity index 100% rename from app/src/main/res/drawable/rightangle_silhoutte.png rename to app/src/main/res/drawable/rightangle_silhouette.png diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cf5d5d..2db5137 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,4 +82,6 @@ Left Angle View Right Angle View Angle View - \ No newline at end of file + + Distance Method + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6575f8b..3133e04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ pagingCommon = "3.3.6" ui = "1.9.5" kotlinxSerialization = "1.6.3" koin = "4.1.1" +window = "1.5.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -42,6 +43,7 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } +androidx-window = { group = "androidx.window", name = "window", version.ref = "window" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }