From ac8e27fbfb914a82f3f3ee501d76a1b773750ae9 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Fri, 29 Aug 2025 15:33:39 +0200 Subject: [PATCH] This commit introduces a new setting in the General Settings screen that allows users to enable or disable haptic feedback (vibration) when a new measurement is received and refactored `BleConnector` to use `MeasurementFacade` for saving measurements. --- android_app/app/src/main/AndroidManifest.xml | 1 + .../openscale/core/facade/BluetoothFacade.kt | 10 +++--- .../openscale/core/facade/SettingsFacade.kt | 18 ++++++++++ .../openscale/core/service/BleConnector.kt | 14 +++----- .../core/usecase/MeasurementCrudUseCases.kt | 31 ++++++++++++++++- .../screen/settings/GeneralSettingsScreen.kt | 33 +++++++++++++++++-- .../app/src/main/res/values-de/strings.xml | 5 +++ .../app/src/main/res/values/strings.xml | 5 +++ 8 files changed, 99 insertions(+), 18 deletions(-) diff --git a/android_app/app/src/main/AndroidManifest.xml b/android_app/app/src/main/AndroidManifest.xml index 9991d5a4..41089c06 100644 --- a/android_app/app/src/main/AndroidManifest.xml +++ b/android_app/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + - if (id == null) flowOf(null) else databaseRepository.getUserById(id) - } + userFacade.observeSelectedUser() .collect { user -> currentAppUser.value = user currentBtScaleUser.value = user?.let { toScaleUser(it) } diff --git a/android_app/app/src/main/java/com/health/openscale/core/facade/SettingsFacade.kt b/android_app/app/src/main/java/com/health/openscale/core/facade/SettingsFacade.kt index ec4db7ff..4b822138 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/facade/SettingsFacade.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/facade/SettingsFacade.kt @@ -60,6 +60,8 @@ object SettingsPreferenceKeys { val CURRENT_USER_ID = intPreferencesKey("current_user_id") val APP_LANGUAGE_CODE = stringPreferencesKey("app_language_code") + val HAPTIC_ON_MEASUREMENT = booleanPreferencesKey("haptic_on_measurement") + // Settings for specific UI components val SELECTED_TYPES_TABLE = stringSetPreferencesKey("selected_types_table") // IDs of measurement types selected for the data table @@ -129,6 +131,9 @@ interface SettingsFacade { val appLanguageCode: Flow suspend fun setAppLanguageCode(languageCode: String?) + val hapticOnMeasurement: Flow + suspend fun setHapticOnMeasurement(value: Boolean) + val currentUserId: Flow suspend fun setCurrentUserId(userId: Int?) @@ -260,6 +265,19 @@ class SettingsFacadeImpl @Inject constructor( } } + override val hapticOnMeasurement: Flow = observeSetting( + SettingsPreferenceKeys.HAPTIC_ON_MEASUREMENT.name, + false + ).catch { exception -> + LogManager.e(TAG, "Error observing hapticOnMeasurement", exception) + emit(false) + } + + override suspend fun setHapticOnMeasurement(value: Boolean) { + LogManager.d(TAG, "Setting hapticOnMeasurement to: $value") + saveSetting(SettingsPreferenceKeys.HAPTIC_ON_MEASUREMENT.name, value) + } + override val currentUserId: Flow = dataStore.data .catch { exception -> LogManager.e(TAG, "Error reading currentUserId from DataStore.", exception) diff --git a/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt b/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt index c0489ee6..65737552 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt @@ -30,6 +30,7 @@ import com.health.openscale.core.data.Measurement import com.health.openscale.core.data.MeasurementTypeKey import com.health.openscale.core.data.MeasurementValue import com.health.openscale.core.database.DatabaseRepository +import com.health.openscale.core.facade.MeasurementFacade import com.health.openscale.core.utils.LogManager import com.health.openscale.ui.shared.SnackbarEvent import kotlinx.coroutines.CoroutineScope @@ -61,14 +62,11 @@ import java.util.concurrent.atomic.AtomicInteger * @param databaseRepository Repository for saving received measurements. * @param sharedViewModel ViewModel for showing snackbars and potentially other UI interactions. * @param getCurrentScaleUser Callback function to retrieve the current Bluetooth scale user. - * @param getCurrentAppUserId Callback function to retrieve the ID of the current application user. - * @param onUserSelectionRequired Callback to notify the UI when user interaction on the device is needed. - * @param onSavePreferredDevice Callback to save the successfully connected device as preferred. */ class BleConnector( private val scope: CoroutineScope, private val scaleFactory: ScaleFactory, - private val databaseRepository: DatabaseRepository, + private val measurementFacade: MeasurementFacade, private val getCurrentScaleUser: () -> ScaleUser?, ) : AutoCloseable { @@ -373,7 +371,7 @@ class BleConnector( // Fetch measurement type IDs from the database to map keys to foreign keys. val typeKeyToIdMap: Map = - databaseRepository.getAllMeasurementTypes().firstOrNull() + measurementFacade.getAllMeasurementTypes().firstOrNull() ?.associate { it.key to it.id } ?: run { LogManager.e(TAG, "Could not load MeasurementTypes from DB for $deviceName.") _snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_measurement_types_not_loaded)) @@ -457,11 +455,9 @@ class BleConnector( } try { - val measurementId = databaseRepository.insertMeasurement(newDbMeasurement) - val finalValues = values.map { it.copy(measurementId = measurementId.toInt()) } - finalValues.forEach { databaseRepository.insertMeasurementValue(it) } + val measurementId = measurementFacade.saveMeasurement(newDbMeasurement, values) - LogManager.i(TAG, "Measurement from $deviceName for User $currentAppUserId saved (ID: $measurementId). Values: ${finalValues.size}") + LogManager.i(TAG, "Measurement from $deviceName for User $currentAppUserId saved (ID: $measurementId). Values: ${values.size}") pendingSavedCount.incrementAndGet() lastSavedArgs = listOf(measurementData.weight, deviceName) savedBurstSignal.tryEmit(Unit) diff --git a/android_app/app/src/main/java/com/health/openscale/core/usecase/MeasurementCrudUseCases.kt b/android_app/app/src/main/java/com/health/openscale/core/usecase/MeasurementCrudUseCases.kt index 86edd485..7b21657d 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/usecase/MeasurementCrudUseCases.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/usecase/MeasurementCrudUseCases.kt @@ -18,9 +18,13 @@ package com.health.openscale.core.usecase import android.content.Context +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager import com.health.openscale.core.data.Measurement import com.health.openscale.core.data.MeasurementValue import com.health.openscale.core.database.DatabaseRepository +import com.health.openscale.core.facade.SettingsFacade import com.health.openscale.ui.widget.MeasurementWidget import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -41,8 +45,10 @@ import javax.inject.Singleton class MeasurementCrudUseCases @Inject constructor( @ApplicationContext private val appContext: Context, private val databaseRepository: DatabaseRepository, - private val sync: SyncUseCases + private val sync: SyncUseCases, + private val settingsFacade: SettingsFacade ) { + private var lastVibrateTime = 0L /** * Inserts or updates a [measurement] and reconciles its [values]. @@ -74,6 +80,8 @@ class MeasurementCrudUseCases @Inject constructor( MeasurementWidget.refreshAll(appContext) + maybeVibrateOnMeasurement() + newId } else { // Update path @@ -127,4 +135,25 @@ class MeasurementCrudUseCases @Inject constructor( suspend fun recalculateDerivedValuesForMeasurement(measurementId: Int) { databaseRepository.recalculateDerivedValuesForMeasurement(measurementId) } + + private suspend fun maybeVibrateOnMeasurement() { + val enabled = runCatching { settingsFacade.hapticOnMeasurement.first() }.getOrDefault(false) + if (!enabled) return + + val now = System.currentTimeMillis() + if (now - lastVibrateTime < 1500) { + return + } + lastVibrateTime = now + + val vm = appContext.getSystemService(VibratorManager::class.java) + val vibrator: Vibrator = vm.defaultVibrator + if (!vibrator.hasVibrator()) return + + val effect = VibrationEffect.createOneShot( + 500L, + 255 + ) + vibrator.vibrate(effect) + } } diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/GeneralSettingsScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/GeneralSettingsScreen.kt index 1fdedb41..e511ed75 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/GeneralSettingsScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/GeneralSettingsScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Vibration import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card @@ -110,6 +111,7 @@ fun GeneralSettingsScreen( val currentLanguageCode by sharedViewModel.appLanguageCode.collectAsState(initial = null) var expandedLanguageMenu by remember { mutableStateOf(false) } + val hapticsEnabled by sharedViewModel.hapticOnMeasurement.collectAsState(initial = false) val selectedLanguage: SupportedLanguage = remember(currentLanguageCode, supportedLanguagesEnumEntries) { val systemDefault = SupportedLanguage.getDefault().code @@ -332,6 +334,34 @@ fun GeneralSettingsScreen( } } + // ---- Haptic section ---- + SettingsSectionTitle(text = stringResource(R.string.settings_feedback_title)) + + SettingsGroup( + leadingIcon = { + Icon( + imageVector = Icons.Filled.Vibration, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + title = stringResource(R.string.settings_haptics_label), + checked = hapticsEnabled, + onCheckedChange = { enabled -> + scope.launch { + sharedViewModel.setHapticOnMeasurement(enabled) + sharedViewModel.showSnackbar( + if (enabled) + context.getString(R.string.settings_haptics_enabled_snackbar) + else + context.getString(R.string.settings_haptics_disabled_snackbar) + ) + } + }, + content = { + } + ) + // --- Reminder --- SettingsSectionTitle(text = stringResource(R.string.settings_reminder_title)) @@ -625,12 +655,11 @@ fun SettingsGroup( } if (checked) { - Spacer(Modifier.height(8.dp)) content() } if (persistentContent != null) { - if (!checked) Spacer(Modifier.height(8.dp)) + if (!checked) persistentContent() } } diff --git a/android_app/app/src/main/res/values-de/strings.xml b/android_app/app/src/main/res/values-de/strings.xml index 3ccaa7d3..d7e94345 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -334,6 +334,11 @@ So Alle + Feedback + Vibration bei neuem Messwert + Vibration bei Messwerten aktiviert + Vibration bei Messwerten deaktiviert + Datenpunkte anzeigen Glättungsalgorithmus diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 56602445..cd68e952 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -336,6 +336,11 @@ Sun All + Feedback + Vibration on new measurement + Vibration on measurements enabled + Vibration on measurements disabled + Show data points Smoothing Algorithm