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