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:
@@ -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"
|
||||||
|
@@ -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) }
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user