1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-31 20:11:58 +02:00

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.

This commit is contained in:
oliexdev
2025-08-29 15:33:39 +02:00
parent 7bce0be76b
commit ac8e27fbfb
8 changed files with 99 additions and 18 deletions

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<permission <permission
android:name="${applicationId}.READ_WRITE_DATA" android:name="${applicationId}.READ_WRITE_DATA"

View File

@@ -59,7 +59,8 @@ import com.health.openscale.ui.shared.SnackbarEvent
class BluetoothFacade @Inject constructor( class BluetoothFacade @Inject constructor(
private val application: Application, private val application: Application,
private val scaleFactory: ScaleFactory, private val scaleFactory: ScaleFactory,
private val databaseRepository: DatabaseRepository, private val measurementFacade: MeasurementFacade,
private val userFacade: UserFacade,
private val settingsFacade: SettingsFacade, private val settingsFacade: SettingsFacade,
) { ) {
private val TAG = "BluetoothFacade" private val TAG = "BluetoothFacade"
@@ -71,7 +72,7 @@ class BluetoothFacade @Inject constructor(
private val connection = BleConnector( private val connection = BleConnector(
scope = scope, scope = scope,
scaleFactory = scaleFactory, scaleFactory = scaleFactory,
databaseRepository = databaseRepository, measurementFacade = measurementFacade,
getCurrentScaleUser = { currentBtScaleUser.value } getCurrentScaleUser = { currentBtScaleUser.value }
) )
@@ -108,10 +109,7 @@ class BluetoothFacade @Inject constructor(
private fun observeCurrentUser() { private fun observeCurrentUser() {
scope.launch { scope.launch {
settingsFacade.currentUserId userFacade.observeSelectedUser()
.flatMapLatest { id ->
if (id == null) flowOf(null) else databaseRepository.getUserById(id)
}
.collect { user -> .collect { user ->
currentAppUser.value = user currentAppUser.value = user
currentBtScaleUser.value = user?.let { toScaleUser(it) } currentBtScaleUser.value = user?.let { toScaleUser(it) }

View File

@@ -60,6 +60,8 @@ object SettingsPreferenceKeys {
val CURRENT_USER_ID = intPreferencesKey("current_user_id") val CURRENT_USER_ID = intPreferencesKey("current_user_id")
val APP_LANGUAGE_CODE = stringPreferencesKey("app_language_code") val APP_LANGUAGE_CODE = stringPreferencesKey("app_language_code")
val HAPTIC_ON_MEASUREMENT = booleanPreferencesKey("haptic_on_measurement")
// Settings for specific UI components // Settings for specific UI components
val SELECTED_TYPES_TABLE = stringSetPreferencesKey("selected_types_table") // IDs of measurement types selected for the data table 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<String?> val appLanguageCode: Flow<String?>
suspend fun setAppLanguageCode(languageCode: String?) suspend fun setAppLanguageCode(languageCode: String?)
val hapticOnMeasurement: Flow<Boolean>
suspend fun setHapticOnMeasurement(value: Boolean)
val currentUserId: Flow<Int?> val currentUserId: Flow<Int?>
suspend fun setCurrentUserId(userId: Int?) suspend fun setCurrentUserId(userId: Int?)
@@ -260,6 +265,19 @@ class SettingsFacadeImpl @Inject constructor(
} }
} }
override val hapticOnMeasurement: Flow<Boolean> = 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<Int?> = dataStore.data override val currentUserId: Flow<Int?> = dataStore.data
.catch { exception -> .catch { exception ->
LogManager.e(TAG, "Error reading currentUserId from DataStore.", exception) LogManager.e(TAG, "Error reading currentUserId from DataStore.", exception)

View File

@@ -30,6 +30,7 @@ import com.health.openscale.core.data.Measurement
import com.health.openscale.core.data.MeasurementTypeKey import com.health.openscale.core.data.MeasurementTypeKey
import com.health.openscale.core.data.MeasurementValue import com.health.openscale.core.data.MeasurementValue
import com.health.openscale.core.database.DatabaseRepository 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.core.utils.LogManager
import com.health.openscale.ui.shared.SnackbarEvent import com.health.openscale.ui.shared.SnackbarEvent
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -61,14 +62,11 @@ import java.util.concurrent.atomic.AtomicInteger
* @param databaseRepository Repository for saving received measurements. * @param databaseRepository Repository for saving received measurements.
* @param sharedViewModel ViewModel for showing snackbars and potentially other UI interactions. * @param sharedViewModel ViewModel for showing snackbars and potentially other UI interactions.
* @param getCurrentScaleUser Callback function to retrieve the current Bluetooth scale user. * @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( class BleConnector(
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val scaleFactory: ScaleFactory, private val scaleFactory: ScaleFactory,
private val databaseRepository: DatabaseRepository, private val measurementFacade: MeasurementFacade,
private val getCurrentScaleUser: () -> ScaleUser?, private val getCurrentScaleUser: () -> ScaleUser?,
) : AutoCloseable { ) : AutoCloseable {
@@ -373,7 +371,7 @@ class BleConnector(
// Fetch measurement type IDs from the database to map keys to foreign keys. // Fetch measurement type IDs from the database to map keys to foreign keys.
val typeKeyToIdMap: Map<MeasurementTypeKey, Int> = val typeKeyToIdMap: Map<MeasurementTypeKey, Int> =
databaseRepository.getAllMeasurementTypes().firstOrNull() measurementFacade.getAllMeasurementTypes().firstOrNull()
?.associate { it.key to it.id } ?: run { ?.associate { it.key to it.id } ?: run {
LogManager.e(TAG, "Could not load MeasurementTypes from DB for $deviceName.") LogManager.e(TAG, "Could not load MeasurementTypes from DB for $deviceName.")
_snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_measurement_types_not_loaded)) _snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_measurement_types_not_loaded))
@@ -457,11 +455,9 @@ class BleConnector(
} }
try { try {
val measurementId = databaseRepository.insertMeasurement(newDbMeasurement) val measurementId = measurementFacade.saveMeasurement(newDbMeasurement, values)
val finalValues = values.map { it.copy(measurementId = measurementId.toInt()) }
finalValues.forEach { databaseRepository.insertMeasurementValue(it) }
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() pendingSavedCount.incrementAndGet()
lastSavedArgs = listOf(measurementData.weight, deviceName) lastSavedArgs = listOf(measurementData.weight, deviceName)
savedBurstSignal.tryEmit(Unit) savedBurstSignal.tryEmit(Unit)

View File

@@ -18,9 +18,13 @@
package com.health.openscale.core.usecase package com.health.openscale.core.usecase
import android.content.Context 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.Measurement
import com.health.openscale.core.data.MeasurementValue import com.health.openscale.core.data.MeasurementValue
import com.health.openscale.core.database.DatabaseRepository import com.health.openscale.core.database.DatabaseRepository
import com.health.openscale.core.facade.SettingsFacade
import com.health.openscale.ui.widget.MeasurementWidget import com.health.openscale.ui.widget.MeasurementWidget
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -41,8 +45,10 @@ import javax.inject.Singleton
class MeasurementCrudUseCases @Inject constructor( class MeasurementCrudUseCases @Inject constructor(
@ApplicationContext private val appContext: Context, @ApplicationContext private val appContext: Context,
private val databaseRepository: DatabaseRepository, 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]. * Inserts or updates a [measurement] and reconciles its [values].
@@ -74,6 +80,8 @@ class MeasurementCrudUseCases @Inject constructor(
MeasurementWidget.refreshAll(appContext) MeasurementWidget.refreshAll(appContext)
maybeVibrateOnMeasurement()
newId newId
} else { } else {
// Update path // Update path
@@ -127,4 +135,25 @@ class MeasurementCrudUseCases @Inject constructor(
suspend fun recalculateDerivedValuesForMeasurement(measurementId: Int) { suspend fun recalculateDerivedValuesForMeasurement(measurementId: Int) {
databaseRepository.recalculateDerivedValuesForMeasurement(measurementId) 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)
}
} }

View File

@@ -44,6 +44,7 @@ import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Vibration
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -110,6 +111,7 @@ fun GeneralSettingsScreen(
val currentLanguageCode by sharedViewModel.appLanguageCode.collectAsState(initial = null) val currentLanguageCode by sharedViewModel.appLanguageCode.collectAsState(initial = null)
var expandedLanguageMenu by remember { mutableStateOf(false) } var expandedLanguageMenu by remember { mutableStateOf(false) }
val hapticsEnabled by sharedViewModel.hapticOnMeasurement.collectAsState(initial = false)
val selectedLanguage: SupportedLanguage = remember(currentLanguageCode, supportedLanguagesEnumEntries) { val selectedLanguage: SupportedLanguage = remember(currentLanguageCode, supportedLanguagesEnumEntries) {
val systemDefault = SupportedLanguage.getDefault().code 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 --- // --- Reminder ---
SettingsSectionTitle(text = stringResource(R.string.settings_reminder_title)) SettingsSectionTitle(text = stringResource(R.string.settings_reminder_title))
@@ -625,12 +655,11 @@ fun SettingsGroup(
} }
if (checked) { if (checked) {
Spacer(Modifier.height(8.dp))
content() content()
} }
if (persistentContent != null) { if (persistentContent != null) {
if (!checked) Spacer(Modifier.height(8.dp)) if (!checked)
persistentContent() persistentContent()
} }
} }

View File

@@ -334,6 +334,11 @@
<string name="sunday_short">So</string> <string name="sunday_short">So</string>
<string name="all">Alle</string> <string name="all">Alle</string>
<string name="settings_feedback_title">Feedback</string>
<string name="settings_haptics_label">Vibration bei neuem Messwert</string>
<string name="settings_haptics_enabled_snackbar">Vibration bei Messwerten aktiviert</string>
<string name="settings_haptics_disabled_snackbar">Vibration bei Messwerten deaktiviert</string>
<!-- Diagramm Einstellungen --> <!-- Diagramm Einstellungen -->
<string name="setting_show_chart_points">Datenpunkte anzeigen</string> <string name="setting_show_chart_points">Datenpunkte anzeigen</string>
<string name="setting_smoothing_algorithm">Glättungsalgorithmus</string> <string name="setting_smoothing_algorithm">Glättungsalgorithmus</string>

View File

@@ -336,6 +336,11 @@
<string name="sunday_short">Sun</string> <string name="sunday_short">Sun</string>
<string name="all">All</string> <string name="all">All</string>
<string name="settings_feedback_title">Feedback</string>
<string name="settings_haptics_label">Vibration on new measurement</string>
<string name="settings_haptics_enabled_snackbar">Vibration on measurements enabled</string>
<string name="settings_haptics_disabled_snackbar">Vibration on measurements disabled</string>
<!-- Chart Settings Screen --> <!-- Chart Settings Screen -->
<string name="setting_show_chart_points">Show data points</string> <string name="setting_show_chart_points">Show data points</string>
<string name="setting_smoothing_algorithm">Smoothing Algorithm</string> <string name="setting_smoothing_algorithm">Smoothing Algorithm</string>