1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-21 07:51:46 +02:00

- Streamlined the handling of user input dialogs (CHOOSE_USER, ENTER_CONSENT) in AppNavigation.kt by resetting dialog-specific state based on interactionEvent.interactionType to ensure fresh state upon dialog visibility.

- Introduced `pendingUserId` to better manage user context during asynchronous operations like consent requests.
- Improved logging for user registration and selection steps.
This commit is contained in:
oliexdev
2025-08-15 07:30:24 +02:00
parent ac181603e1
commit 5bef95e613
5 changed files with 181 additions and 258 deletions

View File

@@ -68,6 +68,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
SharedPreferences prefs; SharedPreferences prefs;
protected boolean registerNewUser; protected boolean registerNewUser;
ScaleUser selectedUser; ScaleUser selectedUser;
private int pendingUserId = -1;
ScaleMeasurement previousMeasurement; ScaleMeasurement previousMeasurement;
protected boolean haveBatteryService; protected boolean haveBatteryService;
protected Vector<ScaleUser> scaleUserList; protected Vector<ScaleUser> scaleUserList;
@@ -174,8 +175,10 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
break; break;
case REGISTER_NEW_SCALE_USER: case REGISTER_NEW_SCALE_USER:
int userId = this.selectedUser.getId(); int userId = this.selectedUser.getId();
pendingUserId = userId;
int consentCode = getUserScaleConsent(userId); int consentCode = getUserScaleConsent(userId);
int userIndex = getUserScaleIndex(userId); int userIndex = getUserScaleIndex(userId);
LogManager.d(TAG, "Step register new scale user, userId: " + userId + ", consentCode: " + consentCode + ", userIndex: " + userIndex);
if (consentCode == -1 || userIndex == -1) { if (consentCode == -1 || userIndex == -1) {
registerNewUser = true; registerNewUser = true;
} }
@@ -188,8 +191,13 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
} }
break; break;
case SELECT_SCALE_USER: case SELECT_SCALE_USER:
LogManager.d(TAG, "Select user on scale!"); int userIdToUse = (pendingUserId != -1) ? pendingUserId : this.selectedUser.getId();
setUser(this.selectedUser.getId()); if (userIdToUse != this.selectedUser.getId()) {
LogManager.w(TAG, "SELECT_SCALE_USER: Using pendingUserId=" + userIdToUse + " (selectedUserId=" + this.selectedUser.getId() + " differs).", null);
} else {
LogManager.d(TAG, "SELECT_SCALE_USER: Using selectedUserId=" + userIdToUse);
}
setUser(userIdToUse);
stopMachineState(); stopMachineState();
break; break;
case SET_SCALE_USER_DATA: case SET_SCALE_USER_DATA:
@@ -293,6 +301,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
} }
if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) { if (value[2] == UDS_CP_RESP_VALUE_SUCCESS) {
LogManager.d(TAG, "UDS_CP_CONSENT: Success user consent"); LogManager.d(TAG, "UDS_CP_CONSENT: Success user consent");
pendingUserId = -1;
resumeMachineState(); resumeMachineState();
} else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) { } else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) {
LogManager.e(TAG, "UDS_CP_CONSENT: Not authorized", null); LogManager.e(TAG, "UDS_CP_CONSENT: Not authorized", null);
@@ -599,7 +608,22 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
protected synchronized void setUser(int userId) { protected synchronized void setUser(int userId) {
int userIndex = getUserScaleIndex(userId); int userIndex = getUserScaleIndex(userId);
int consentCode = getUserScaleConsent(userId); int consentCode = getUserScaleConsent(userId);
LogManager.d(TAG, String.format("setting: userId %d, userIndex: %d, consent Code: %d ", userId, userIndex, consentCode)); LogManager.d(TAG, "setUser(appUserId=" + userId + ") with index=" + userIndex + " consent=" + consentCode
+ " (selectedUserId=" + (this.selectedUser != null ? this.selectedUser.getId() : -1)
+ ", pendingUserId=" + pendingUserId + ")");
if (userIndex == -1) {
LogManager.w(TAG, "setUser: no scale index for appUserId=" + userId + ". Requesting vendor-specific user list.", null);
jumpNextToStepNr(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST.ordinal());
stopMachineState();
requestVendorSpecificUserList();
return;
}
if (consentCode == -1) {
LogManager.w(TAG, "setUser: missing consent for appUserId=" + userId + " (index=" + userIndex + "). Requesting consent.", null);
requestScaleUserConsent(userId, userIndex);
return;
}
setUser(userIndex, consentCode); setUser(userIndex, consentCode);
} }
@@ -678,6 +702,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
protected synchronized void storeUserScaleConsentCode(int userId, int consentCode) { protected synchronized void storeUserScaleConsentCode(int userId, int consentCode) {
prefs.edit().putInt("userConsentCode" + userId, consentCode).apply(); prefs.edit().putInt("userConsentCode" + userId, consentCode).apply();
LogManager.d(TAG, "storeUserScaleConsentCode: userId=" + userId + " now=" + getUserScaleConsent(userId));
} }
protected synchronized int getUserScaleConsent(int userId) { protected synchronized int getUserScaleConsent(int userId) {
@@ -687,12 +712,13 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
protected synchronized void storeUserScaleIndex(int userId, int userIndex) { protected synchronized void storeUserScaleIndex(int userId, int userIndex) {
int currentUserIndex = getUserScaleIndex(userId); int currentUserIndex = getUserScaleIndex(userId);
if (currentUserIndex != -1) { if (currentUserIndex != -1) {
prefs.edit().putInt("userIdFromUserScaleIndex" + currentUserIndex, -1); prefs.edit().putInt("userIdFromUserScaleIndex" + currentUserIndex, -1).apply();
} }
prefs.edit().putInt("userScaleIndex" + userId, userIndex).apply(); prefs.edit().putInt("userScaleIndex" + userId, userIndex).apply();
if (userIndex != -1) { if (userIndex != -1) {
prefs.edit().putInt("userIdFromUserScaleIndex" + userIndex, userId).apply(); prefs.edit().putInt("userIdFromUserScaleIndex" + userIndex, userId).apply();
} }
LogManager.d(TAG, "storeUserScaleIndex: userId=" + userId + " now=" + getUserScaleIndex(userId));
} }
protected synchronized int getUserIdFromScaleIndex(int userScaleIndex) { protected synchronized int getUserIdFromScaleIndex(int userScaleIndex) {
@@ -717,13 +743,17 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
} }
protected void requestScaleUserConsent(int appScaleUserId, int scaleUserIndex) { protected void requestScaleUserConsent(int appScaleUserId, int scaleUserIndex) {
pendingUserId = appScaleUserId;
LogManager.d(TAG, "requestScaleUserConsent(appUserId=" + appScaleUserId + ", scaleIndex=" + scaleUserIndex + "), pendingUserId=" + pendingUserId);
Object[] consentRequestData = new Object[]{appScaleUserId, scaleUserIndex}; Object[] consentRequestData = new Object[]{appScaleUserId, scaleUserIndex};
requestUserInteraction(UserInteractionType.ENTER_CONSENT, consentRequestData); requestUserInteraction(UserInteractionType.ENTER_CONSENT, consentRequestData);
} }
@Override @Override
public void processUserInteractionFeedback(UserInteractionType interactionType, int appUserId, Object feedbackData, Handler uiHandler) { public void processUserInteractionFeedback(UserInteractionType interactionType, int appUserId, Object feedbackData, Handler uiHandler) {
LogManager.d(TAG, "Processing UserInteractionFeedback: " + interactionType + " for appUserId: " + appUserId); pendingUserId = appUserId;
LogManager.d(TAG, "Processing UserInteractionFeedback: " + interactionType + " for appUserId=" + appUserId + " (pendingUserId set)");
switch (interactionType) { switch (interactionType) {
case CHOOSE_USER: case CHOOSE_USER:
if (feedbackData instanceof Integer) { if (feedbackData instanceof Integer) {
@@ -748,6 +778,8 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
int scaleUserConsent = (Integer) feedbackData; int scaleUserConsent = (Integer) feedbackData;
LogManager.d(TAG, "ENTER_CONSENT Feedback: scaleUserConsent = " + scaleUserConsent); LogManager.d(TAG, "ENTER_CONSENT Feedback: scaleUserConsent = " + scaleUserConsent);
storeUserScaleConsentCode(appUserId, scaleUserConsent); storeUserScaleConsentCode(appUserId, scaleUserConsent);
LogManager.d(TAG, "after_enter_consent_store: appUserId=" + appUserId + ", pendingUserId=" + pendingUserId
+ ", selectedUserId=" + (this.selectedUser != null ? this.selectedUser.getId() : -1));
if (scaleUserConsent == -1) { // User cancelled or denied consent if (scaleUserConsent == -1) { // User cancelled or denied consent
reconnectOrSetSmState(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler); reconnectOrSetSmState(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
} else { // User provided consent } else { // User provided consent
@@ -769,8 +801,20 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
int userListStatus = parser.getIntValue(FORMAT_UINT8); int userListStatus = parser.getIntValue(FORMAT_UINT8);
if (userListStatus == 2) { if (userListStatus == 2) {
LogManager.d(TAG, "scale have no users!"); LogManager.d(TAG, "scale have no users!");
storeUserScaleConsentCode(selectedUser.getId(), -1); int uid = selectedUser.getId();
storeUserScaleIndex(selectedUser.getId(), -1);
int oldConsent = getUserScaleConsent(uid);
if (oldConsent != -1) {
LogManager.w(TAG, "Status=2 -> resetting consent for userId=" + uid + " from " + oldConsent + " to -1", null);
storeUserScaleConsentCode(uid, -1);
}
int oldIndex = getUserScaleIndex(uid);
if (oldIndex != -1) {
LogManager.w(TAG, "Status=2 -> resetting index for userId=" + uid + " from " + oldIndex + " to -1", null);
storeUserScaleIndex(uid, -1);
}
jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal()); jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal());
resumeMachineState(); resumeMachineState();
return; return;
@@ -782,9 +826,8 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
} }
LogManager.d(TAG, "\n" + (i + 1) + ". " + scaleUserList.get(i)); LogManager.d(TAG, "\n" + (i + 1) + ". " + scaleUserList.get(i));
} }
if ((scaleUserList.size() == 0)) { if (scaleUserList.size() == 0) {
storeUserScaleConsentCode(selectedUser.getId(), -1); LogManager.w(TAG, "status=1 but user list empty; skipping forced reset of consent/index.", null);
storeUserScaleIndex(selectedUser.getId(), -1);
jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal()); jumpNextToStepNr(SM_STEPS.REGISTER_NEW_SCALE_USER.ordinal());
resumeMachineState(); resumeMachineState();
return; return;
@@ -889,7 +932,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
LogManager.e(TAG, "CHOOSE_USER: choiceStrings or indexArray is null. Cannot request user interaction.", null); LogManager.e(TAG, "CHOOSE_USER: choiceStrings or indexArray is null. Cannot request user interaction.", null);
} }
Pair<CharSequence[], int[]> choices = new Pair(choiceStrings, indexArray); Pair<CharSequence[], int[]> choices = new Pair<>(choiceStrings, indexArray);
requestUserInteraction(UserInteractionType.CHOOSE_USER, choices); requestUserInteraction(UserInteractionType.CHOOSE_USER, choices);
} }

View File

@@ -247,25 +247,17 @@ fun AppNavigation(sharedViewModel: SharedViewModel) {
val dialogTitle: String val dialogTitle: String
val dialogIcon: @Composable (() -> Unit) val dialogIcon: @Composable (() -> Unit)
var consentCodeInput by rememberSaveable { mutableStateOf("") } var consentCodeInput by rememberSaveable(interactionEvent.interactionType) { mutableStateOf("") }
var selectedUserIndexState by rememberSaveable { mutableIntStateOf(Int.MIN_VALUE) } var selectedUserIndexState by rememberSaveable(interactionEvent.interactionType) { mutableIntStateOf(Int.MIN_VALUE) }
when (interactionEvent.interactionType) { when (interactionEvent.interactionType) {
UserInteractionType.CHOOSE_USER -> { UserInteractionType.CHOOSE_USER -> {
dialogTitle = stringResource(R.string.dialog_bt_interaction_title_choose_user) dialogTitle = stringResource(R.string.dialog_bt_interaction_title_choose_user)
dialogIcon = { Icon(Icons.Filled.People, contentDescription = stringResource(R.string.dialog_bt_icon_desc_choose_user)) } dialogIcon = { Icon(Icons.Filled.People, contentDescription = stringResource(R.string.dialog_bt_icon_desc_choose_user)) }
// Reset state when dialog becomes visible for CHOOSE_USER
if (interactionEvent.interactionType == UserInteractionType.CHOOSE_USER) {
selectedUserIndexState = Int.MIN_VALUE
}
} }
UserInteractionType.ENTER_CONSENT -> { UserInteractionType.ENTER_CONSENT -> {
dialogTitle = stringResource(R.string.dialog_bt_interaction_title_enter_consent) dialogTitle = stringResource(R.string.dialog_bt_interaction_title_enter_consent)
dialogIcon = { Icon(Icons.Filled.HowToReg, contentDescription = stringResource(R.string.dialog_bt_icon_desc_enter_consent)) } dialogIcon = { Icon(Icons.Filled.HowToReg, contentDescription = stringResource(R.string.dialog_bt_icon_desc_enter_consent)) }
// Reset state when dialog becomes visible for ENTER_CONSENT
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) {
consentCodeInput = ""
}
} }
// else -> { /* Handle unknown types or provide defaults */ } // Optional // else -> { /* Handle unknown types or provide defaults */ } // Optional
} }
@@ -276,9 +268,6 @@ fun AppNavigation(sharedViewModel: SharedViewModel) {
AlertDialog( AlertDialog(
onDismissRequest = { onDismissRequest = {
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) {
bluetoothViewModel.processUserInteraction(interactionEvent.interactionType, -1) // -1 abort signal
}
bluetoothViewModel.clearPendingUserInteraction() bluetoothViewModel.clearPendingUserInteraction()
}, },
icon = dialogIcon, icon = dialogIcon,
@@ -301,9 +290,9 @@ fun AppNavigation(sharedViewModel: SharedViewModel) {
LogManager.d(TAG, "CHOOSE_USER interaction received. Data: $choicesData") LogManager.d(TAG, "CHOOSE_USER interaction received. Data: $choicesData")
// Expecting Pair<Array<String>, IntArray> or Pair<Array<CharSequence>, IntArray> // Expecting Pair<Array<String>, IntArray> or Pair<Array<CharSequence>, IntArray>
if (choicesData is Pair<*, *> && choicesData.first is Array<*> && choicesData.second is IntArray) { if (choicesData is android.util.Pair<*, *> && choicesData.first is Array<*> && choicesData.second is IntArray) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val choices = choicesData as Pair<Array<CharSequence>, IntArray> val choices = choicesData as android.util.Pair<Array<CharSequence>, IntArray>
val choiceDisplayNames = choices.first val choiceDisplayNames = choices.first
val choiceIndices = choices.second val choiceIndices = choices.second
@@ -395,11 +384,7 @@ fun AppNavigation(sharedViewModel: SharedViewModel) {
dismissButton = { dismissButton = {
TextButton( TextButton(
onClick = { onClick = {
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) { bluetoothViewModel.clearPendingUserInteraction()
bluetoothViewModel.processUserInteraction(interactionEvent.interactionType, -1) // -1 for abort signal
} else {
bluetoothViewModel.clearPendingUserInteraction()
}
} }
) { ) {
Text(stringResource(R.string.cancel_button)) Text(stringResource(R.string.cancel_button))

View File

@@ -26,24 +26,18 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.compose.material3.SnackbarDuration // Keep if used directly, otherwise remove import androidx.compose.material3.SnackbarDuration
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.health.openscale.R
import com.health.openscale.core.bluetooth.BluetoothEvent import com.health.openscale.core.bluetooth.BluetoothEvent
// ScaleCommunicator no longer needed directly here
import com.health.openscale.core.bluetooth.ScaleFactory import com.health.openscale.core.bluetooth.ScaleFactory
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
// ScaleMeasurement no longer needed directly here for saveMeasurementFromEvent
import com.health.openscale.core.bluetooth.data.ScaleUser import com.health.openscale.core.bluetooth.data.ScaleUser
import com.health.openscale.core.bluetooth.scalesJava.BluetoothCommunication
// Measurement, MeasurementTypeKey, MeasurementValue no longer needed directly here
import com.health.openscale.core.data.User import com.health.openscale.core.data.User
import com.health.openscale.core.utils.LogManager import com.health.openscale.core.utils.LogManager
import com.health.openscale.ui.screen.SharedViewModel import com.health.openscale.ui.screen.SharedViewModel
// kotlinx.coroutines.Dispatchers no longer needed directly here for saveMeasurement
// kotlinx.coroutines.Job no longer needed directly here for communicatorJob
// kotlinx.coroutines.delay no longer needed directly here for disconnect-timeout
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -59,60 +53,33 @@ import java.util.Date
* Represents the various states of a Bluetooth connection. * Represents the various states of a Bluetooth connection.
*/ */
enum class ConnectionStatus { enum class ConnectionStatus {
/** No connection activity. */ NONE, IDLE, DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING, FAILED
NONE,
/** Bluetooth adapter is present and enabled, but not actively scanning or connected. */
IDLE,
/** No active connection to a device. */
DISCONNECTED,
/** Attempting to establish a connection to a device. */
CONNECTING,
/** Successfully connected to a device. */
CONNECTED,
/** In the process of disconnecting from a device. */
DISCONNECTING,
/** A connection attempt or an established connection has failed. */
FAILED
} }
/** /**
* ViewModel responsible for managing Bluetooth interactions, including device scanning, * ViewModel for Bluetooth interactions: scanning, connection, data handling.
* connection, and data handling. It coordinates with [BluetoothScannerManager] for scanning * Coordinates with [BluetoothScannerManager] and [BluetoothConnectionManager].
* and [BluetoothConnectionManager] for connection lifecycle and data events.
*
* This ViewModel also manages user context relevant to Bluetooth operations and exposes
* StateFlows for UI observation.
*
* @param application The application context.
* @param sharedViewModel A [SharedViewModel] instance for accessing shared resources like
* repositories and for displaying global UI messages (e.g., Snackbars).
*/ */
class BluetoothViewModel( class BluetoothViewModel(
private val application: Application, private val application: Application, // Used for context and string resources
val sharedViewModel: SharedViewModel val sharedViewModel: SharedViewModel
) : ViewModel() { ) : ViewModel() {
private companion object { private companion object {
const val TAG = "BluetoothViewModel" const val TAG = "BluetoothViewModel"
const val SCAN_DURATION_MS = 20000L // Default scan duration: 20 seconds const val SCAN_DURATION_MS = 20000L
} }
// Access to repositories is passed to the managers.
private val databaseRepository = sharedViewModel.databaseRepository private val databaseRepository = sharedViewModel.databaseRepository
val userSettingsRepository = sharedViewModel.userSettingRepository val userSettingsRepository = sharedViewModel.userSettingRepository
// --- User Context (managed by ViewModel, used by ConnectionManager) ---
private var currentAppUser: User? = null private var currentAppUser: User? = null
private var currentBtScaleUser: ScaleUser? = null // Derived from currentAppUser for Bluetooth operations private var currentBtScaleUser: ScaleUser? = null
private var currentAppUserId: Int = 0 private var currentAppUserId: Int = 0
// --- Dependencies (ScaleFactory is passed to managers) ---
private val scaleFactory = ScaleFactory(application.applicationContext, databaseRepository) private val scaleFactory = ScaleFactory(application.applicationContext, databaseRepository)
// --- BluetoothScannerManager (manages device scanning) ---
private val bluetoothScannerManager = BluetoothScannerManager(application, viewModelScope, scaleFactory) private val bluetoothScannerManager = BluetoothScannerManager(application, viewModelScope, scaleFactory)
// --- BluetoothConnectionManager (manages device connection and data events) ---
private val bluetoothConnectionManager = BluetoothConnectionManager( private val bluetoothConnectionManager = BluetoothConnectionManager(
context = application.applicationContext, context = application.applicationContext,
scope = viewModelScope, scope = viewModelScope,
@@ -121,76 +88,51 @@ class BluetoothViewModel(
sharedViewModel = sharedViewModel, sharedViewModel = sharedViewModel,
getCurrentScaleUser = { currentBtScaleUser }, getCurrentScaleUser = { currentBtScaleUser },
onSavePreferredDevice = { address, name -> onSavePreferredDevice = { address, name ->
// Save preferred device when ConnectionManager successfully connects and indicates to do so. // Snackbar for user feedback when a device is set as preferred by ConnectionManager
// Snackbar for user feedback can be shown here or in ConnectionManager; here is fine. sharedViewModel.showSnackbar(
viewModelScope.launch { application.getString(R.string.bt_snackbar_scale_saved_as_preferred, name),
userSettingsRepository.saveBluetoothScale(address, name) SnackbarDuration.Short
sharedViewModel.showSnackbar("$name saved as preferred scale.", SnackbarDuration.Short) )
}
} }
) )
// --- Scan State Flows (from BluetoothScannerManager) ---
/** Emits the list of discovered Bluetooth devices. */
val scannedDevices: StateFlow<List<ScannedDeviceInfo>> = bluetoothScannerManager.scannedDevices val scannedDevices: StateFlow<List<ScannedDeviceInfo>> = bluetoothScannerManager.scannedDevices
/** Emits `true` if a Bluetooth scan is currently active, `false` otherwise. */
val isScanning: StateFlow<Boolean> = bluetoothScannerManager.isScanning val isScanning: StateFlow<Boolean> = bluetoothScannerManager.isScanning
/** Emits error messages related to the scanning process, or null if no error. */
val scanError: StateFlow<String?> = bluetoothScannerManager.scanError val scanError: StateFlow<String?> = bluetoothScannerManager.scanError
// --- Connection State Flows (from BluetoothConnectionManager) ---
/** Emits the MAC address of the currently connected device, or null if not connected. */
val connectedDeviceAddress: StateFlow<String?> = bluetoothConnectionManager.connectedDeviceAddress val connectedDeviceAddress: StateFlow<String?> = bluetoothConnectionManager.connectedDeviceAddress
/** Emits the current [ConnectionStatus] of the Bluetooth device. */
val connectionStatus: StateFlow<ConnectionStatus> = bluetoothConnectionManager.connectionStatus val connectionStatus: StateFlow<ConnectionStatus> = bluetoothConnectionManager.connectionStatus
/** Emits connection-related error messages, or null if no error. */
val connectionError: StateFlow<String?> = bluetoothConnectionManager.connectionError val connectionError: StateFlow<String?> = bluetoothConnectionManager.connectionError
// --- Permissions and System State (managed by ViewModel) ---
private val _permissionsGranted = MutableStateFlow(checkInitialPermissions()) private val _permissionsGranted = MutableStateFlow(checkInitialPermissions())
/** Emits `true` if all necessary Bluetooth permissions are granted, `false` otherwise. */
val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow() val permissionsGranted: StateFlow<Boolean> = _permissionsGranted.asStateFlow()
// --- Saved Device Info (for UI display and auto-connect logic) ---
/** Emits the MAC address of the saved preferred Bluetooth scale, or null if none is saved. */
val savedScaleAddress: StateFlow<String?> = userSettingsRepository.savedBluetoothScaleAddress val savedScaleAddress: StateFlow<String?> = userSettingsRepository.savedBluetoothScaleAddress
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null)
/** Emits the name of the saved preferred Bluetooth scale, or null if none is saved. */
val savedScaleName: StateFlow<String?> = userSettingsRepository.savedBluetoothScaleName val savedScaleName: StateFlow<String?> = userSettingsRepository.savedBluetoothScaleName
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null)
// --- UI Interaction for User Selection (triggered by ConnectionManager callback) ---
val pendingUserInteractionEvent: StateFlow<BluetoothEvent.UserInteractionRequired?> = val pendingUserInteractionEvent: StateFlow<BluetoothEvent.UserInteractionRequired?> =
bluetoothConnectionManager.userInteractionRequiredEvent bluetoothConnectionManager.userInteractionRequiredEvent
init { init {
LogManager.i(TAG, "ViewModel initialized. Setting up user observation.") LogManager.i(TAG, "ViewModel initialized. Setting up user observation.")
observeUserChanges() observeUserChanges()
// attemptAutoConnectToSavedScale() // Can be enabled if auto-connect on ViewModel init is desired.
} }
/**
* Observes changes to the selected application user and updates the Bluetooth user context accordingly.
* This ensures that operations like saving measurements or providing user data to the scale
* use the correct user profile.
*/
private fun observeUserChanges() { private fun observeUserChanges() {
viewModelScope.launch { viewModelScope.launch {
// Observe user selected via SharedViewModel (e.g., user picker in UI)
sharedViewModel.selectedUser.filterNotNull().collectLatest { appUser -> sharedViewModel.selectedUser.filterNotNull().collectLatest { appUser ->
LogManager.d(TAG, "User selected via SharedViewModel: ${appUser.name}. Updating context.") LogManager.d(TAG, "User selected via SharedViewModel: ${appUser.name}. Updating context.")
updateCurrentUserContext(appUser) updateCurrentUserContext(appUser)
} }
} }
viewModelScope.launch { viewModelScope.launch {
// Fallback: Observe current user ID from settings if no user is selected via SharedViewModel.
// This handles scenarios where the app starts and a default user is already set.
if (sharedViewModel.selectedUser.value == null) { if (sharedViewModel.selectedUser.value == null) {
userSettingsRepository.currentUserId.filterNotNull().collectLatest { userId -> userSettingsRepository.currentUserId.filterNotNull().collectLatest { userId ->
if (userId != 0) { if (userId != 0) {
databaseRepository.getUserById(userId).filterNotNull().firstOrNull()?.let { userDetails -> databaseRepository.getUserById(userId).filterNotNull().firstOrNull()?.let { userDetails ->
if (currentAppUserId != userDetails.id) { // Only update if the user actually changed. if (currentAppUserId != userDetails.id) {
LogManager.d(TAG, "User changed via UserSettingsRepository: ${userDetails.name}. Updating context.") LogManager.d(TAG, "User changed via UserSettingsRepository: ${userDetails.name}. Updating context.")
updateCurrentUserContext(userDetails) updateCurrentUserContext(userDetails)
} }
@@ -207,10 +149,6 @@ class BluetoothViewModel(
} }
} }
/**
* Updates the internal state for the current application user and the corresponding Bluetooth scale user.
* @param appUser The [User] object representing the current application user.
*/
private fun updateCurrentUserContext(appUser: User) { private fun updateCurrentUserContext(appUser: User) {
currentAppUser = appUser currentAppUser = appUser
currentAppUserId = appUser.id currentAppUserId = appUser.id
@@ -218,9 +156,6 @@ class BluetoothViewModel(
LogManager.i(TAG, "User context updated for Bluetooth operations: User '${currentBtScaleUser?.userName}' (App ID: ${currentAppUserId})") LogManager.i(TAG, "User context updated for Bluetooth operations: User '${currentBtScaleUser?.userName}' (App ID: ${currentAppUserId})")
} }
/**
* Clears the current user context. Called when no user is selected or found.
*/
private fun clearUserContext() { private fun clearUserContext() {
currentAppUser = null currentAppUser = null
currentAppUserId = 0 currentAppUserId = 0
@@ -228,72 +163,48 @@ class BluetoothViewModel(
LogManager.i(TAG, "User context cleared for Bluetooth operations.") LogManager.i(TAG, "User context cleared for Bluetooth operations.")
} }
/**
* Converts an application [User] object to a [ScaleUser] object,
* which is the format expected by some Bluetooth scale drivers.
* @param appUser The application [User] to convert.
* @return A [ScaleUser] representation.
*/
private fun convertAppUserToBtScaleUser(appUser: User): ScaleUser { private fun convertAppUserToBtScaleUser(appUser: User): ScaleUser {
return ScaleUser().apply { return ScaleUser().apply {
// Note: ScaleUser.id often corresponds to the on-scale user slot (1-N),
// while appUser.id is the database ID. Some drivers might use appUser.id directly
// if the scale supports arbitrary user identifiers or if we manage mapping externally.
// For now, using appUser.id as a general identifier for the ScaleUser.
id = appUser.id id = appUser.id
userName = appUser.name userName = appUser.name
birthday = Date(appUser.birthDate) // Ensure birthDate is in millis birthday = Date(appUser.birthDate)
bodyHeight = appUser.heightCm ?: 0f // Default to 0f if height is null bodyHeight = appUser.heightCm ?: 0f
gender = appUser.gender gender = appUser.gender
} }
} }
// --- Scan Control --- @SuppressLint("MissingPermission")
/**
* Requests the [BluetoothScannerManager] to start scanning for devices.
* Checks for necessary permissions and Bluetooth enabled status before initiating the scan.
*/
@SuppressLint("MissingPermission") // Permissions are checked before calling the manager.
fun requestStartDeviceScan() { fun requestStartDeviceScan() {
LogManager.i(TAG, "User requested to start device scan.") LogManager.i(TAG, "User requested to start device scan.")
refreshPermissionsStatus() // Ensure permission state is up-to-date. refreshPermissionsStatus()
if (!permissionsGranted.value) { if (!permissionsGranted.value) {
LogManager.w(TAG, "Scan request denied: Bluetooth permissions missing.") LogManager.w(TAG, "Scan request denied: Bluetooth permissions missing.")
sharedViewModel.showSnackbar("Bluetooth permissions are required to scan for devices.", SnackbarDuration.Long) sharedViewModel.showSnackbar(
application.getString(R.string.bt_snackbar_permissions_required_to_scan),
SnackbarDuration.Long
)
return return
} }
if (!isBluetoothEnabled()) { if (!isBluetoothEnabled()) {
LogManager.w(TAG, "Scan request denied: Bluetooth is disabled.") LogManager.w(TAG, "Scan request denied: Bluetooth is disabled.")
sharedViewModel.showSnackbar("Bluetooth is disabled. Please enable it to scan for devices.", SnackbarDuration.Long) sharedViewModel.showSnackbar(
application.getString(R.string.bt_snackbar_bluetooth_disabled_to_scan),
SnackbarDuration.Long
)
return return
} }
clearAllErrors() // Clear previous scan/connection errors. clearAllErrors()
LogManager.d(TAG, "Prerequisites met. Delegating scan start to BluetoothScannerManager.") LogManager.d(TAG, "Prerequisites met. Delegating scan start to BluetoothScannerManager.")
bluetoothScannerManager.startScan(SCAN_DURATION_MS) bluetoothScannerManager.startScan(SCAN_DURATION_MS)
} }
/**
* Requests the [BluetoothScannerManager] to stop an ongoing device scan.
*/
fun requestStopDeviceScan() { fun requestStopDeviceScan() {
LogManager.i(TAG, "User requested to stop device scan. Delegating to BluetoothScannerManager.") LogManager.i(TAG, "User requested to stop device scan. Delegating to BluetoothScannerManager.")
// The `isTimeout` parameter is an internal detail for the scanner manager;
// from ViewModel's perspective, it's a manual stop request.
bluetoothScannerManager.stopScan() bluetoothScannerManager.stopScan()
} }
// --- Connection Control --- @SuppressLint("MissingPermission")
/**
* Initiates a connection attempt to the specified Bluetooth device.
* If a scan is active, it will be stopped first.
* Prerequisites like permissions and Bluetooth status are validated.
*
* @param deviceInfo The [ScannedDeviceInfo] of the device to connect to.
*/
@SuppressLint("MissingPermission") // Permissions are checked by validateConnectionPrerequisites.
fun connectToDevice(deviceInfo: ScannedDeviceInfo) { fun connectToDevice(deviceInfo: ScannedDeviceInfo) {
val deviceDisplayName = deviceInfo.name ?: deviceInfo.address val deviceDisplayName = deviceInfo.name ?: deviceInfo.address
LogManager.i(TAG, "User requested to connect to device: $deviceDisplayName") LogManager.i(TAG, "User requested to connect to device: $deviceDisplayName")
@@ -301,13 +212,9 @@ class BluetoothViewModel(
if (isScanning.value) { if (isScanning.value) {
LogManager.d(TAG, "Scan is active, stopping it before initiating connection to $deviceDisplayName.") LogManager.d(TAG, "Scan is active, stopping it before initiating connection to $deviceDisplayName.")
requestStopDeviceScan() requestStopDeviceScan()
// Optional: A small delay could be added here if needed to ensure scan stop completes,
// but usually the managers handle sequential operations gracefully.
// viewModelScope.launch { delay(200) }
} }
if (!validateConnectionPrerequisites(deviceDisplayName, isManualConnect = true)) { if (!validateConnectionPrerequisites(deviceDisplayName, isManualConnect = true)) {
// validateConnectionPrerequisites logs and shows Snackbar for errors.
return return
} }
@@ -315,13 +222,7 @@ class BluetoothViewModel(
bluetoothConnectionManager.connectToDevice(deviceInfo) bluetoothConnectionManager.connectToDevice(deviceInfo)
} }
@SuppressLint("MissingPermission")
/**
* Attempts to connect to the saved preferred Bluetooth scale.
* Retrieves device info from [userSettingsRepository] and then delegates
* to [BluetoothConnectionManager].
*/
@SuppressLint("MissingPermission") // Permissions are checked by validateConnectionPrerequisites.
fun connectToSavedDevice() { fun connectToSavedDevice() {
viewModelScope.launch { viewModelScope.launch {
val address = savedScaleAddress.value val address = savedScaleAddress.value
@@ -331,80 +232,70 @@ class BluetoothViewModel(
if (isScanning.value) { if (isScanning.value) {
LogManager.d(TAG, "Scan is active, stopping it before connecting to saved device '$name'.") LogManager.d(TAG, "Scan is active, stopping it before connecting to saved device '$name'.")
requestStopDeviceScan() requestStopDeviceScan()
// delay(200) // Optional delay
} }
if (!validateConnectionPrerequisites(name, isManualConnect = false)) { if (!validateConnectionPrerequisites(name, isManualConnect = false)) {
// If isManualConnect is false, validateConnectionPrerequisites shows a Snackbar
// but doesn't set an error in ConnectionManager, which is fine for auto-attempts.
return@launch return@launch
} }
if (address != null && name != null) { if (address != null && name != null) {
// For a saved device, we need to re-evaluate its support status using ScaleFactory,
// as supported handlers might change with app updates.
LogManager.d(TAG, "Re-evaluating support for saved device '$name' ($address) using ScaleFactory.") LogManager.d(TAG, "Re-evaluating support for saved device '$name' ($address) using ScaleFactory.")
val deviceInfoForConnect = ScannedDeviceInfo( val deviceInfoForConnect = ScannedDeviceInfo(
name = name, name = name, address = address, rssi = 0, serviceUuids = emptyList(),
address = address, manufacturerData = null, isSupported = false, determinedHandlerDisplayName = null
rssi = 0, // RSSI is not relevant for a direct connection attempt to a saved device.
serviceUuids = emptyList(),
manufacturerData = null,
isSupported = false, // will be determined by getSupportingHandlerInfo
determinedHandlerDisplayName = null // will be determined by getSupportingHandlerInfo
) )
val (isPotentiallySupported, handlerNameFromFactory) = scaleFactory.getSupportingHandlerInfo(deviceInfoForConnect) val (isPotentiallySupported, handlerNameFromFactory) = scaleFactory.getSupportingHandlerInfo(deviceInfoForConnect)
deviceInfoForConnect.isSupported = isPotentiallySupported deviceInfoForConnect.isSupported = isPotentiallySupported
deviceInfoForConnect.determinedHandlerDisplayName = handlerNameFromFactory deviceInfoForConnect.determinedHandlerDisplayName = handlerNameFromFactory
if (!deviceInfoForConnect.isSupported) { if (!deviceInfoForConnect.isSupported) {
LogManager.w(TAG, "Saved device '$name' ($address) is currently not supported by ScaleFactory. Connection aborted.") LogManager.w(TAG, "Saved device '$name' ($address) is currently not supported. Connection aborted.")
// This error is specific to connecting to a *saved* device that's no longer supported. sharedViewModel.showSnackbar(
// The ConnectionManager might not have a dedicated error state for this nuance if it only expects application.getString(R.string.bt_snackbar_saved_scale_no_longer_supported, name),
// ScannedDeviceInfo for connection attempts. Showing a Snackbar is a direct user feedback. SnackbarDuration.Long
sharedViewModel.showSnackbar("Saved scale '$name' is no longer supported.", SnackbarDuration.Long) )
// We don't want to set a generic connectionError in BluetoothConnectionManager here,
// as no connection attempt was made *through* it yet.
return@launch return@launch
} }
LogManager.d(TAG, "Saved device '$name' is supported. Delegating connection to BluetoothConnectionManager.") LogManager.d(TAG, "Saved device '$name' is supported. Delegating connection to BluetoothConnectionManager.")
bluetoothConnectionManager.connectToDevice(deviceInfoForConnect) bluetoothConnectionManager.connectToDevice(deviceInfoForConnect)
} else { } else {
LogManager.w(TAG, "Attempted to connect to saved device, but no device is saved.") LogManager.w(TAG, "Attempted to connect to saved device, but no device is saved.")
sharedViewModel.showSnackbar("No Bluetooth scale saved in settings.", SnackbarDuration.Short) sharedViewModel.showSnackbar(
application.getString(R.string.bt_snackbar_no_scale_saved),
SnackbarDuration.Short
)
} }
} }
} }
/** /**
* Validates common prerequisites for initiating a Bluetooth connection. * Validates common prerequisites for initiating a Bluetooth connection.
* Checks for permissions and Bluetooth enabled status.
*
* @param deviceName The name/identifier of the device for logging/messages.
* @param isManualConnect `true` if this is a direct user action to connect, `false` for automated attempts.
* This influences how errors are reported (e.g., setting an error in ConnectionManager vs. just a Snackbar).
* @return `true` if all prerequisites are met, `false` otherwise. * @return `true` if all prerequisites are met, `false` otherwise.
*/ */
private fun validateConnectionPrerequisites(deviceName: String?, isManualConnect: Boolean): Boolean { private fun validateConnectionPrerequisites(deviceNameForMessage: String?, isManualConnect: Boolean): Boolean {
refreshPermissionsStatus() // Always get the latest permission status. refreshPermissionsStatus()
val devicePlaceholder = application.getString(R.string.device_placeholder_name) // "the device"
if (!permissionsGranted.value) { if (!permissionsGranted.value) {
val errorMsg = "Bluetooth permissions are required to connect to ${deviceName ?: "the device"}." val errorMsg = application.getString(
LogManager.w(TAG, "Connection prerequisite failed for '${deviceName ?: "device"}': $errorMsg") R.string.bt_snackbar_permissions_required_to_connect,
deviceNameForMessage ?: devicePlaceholder
)
LogManager.w(TAG, "Connection prerequisite failed: $errorMsg")
if (isManualConnect) { if (isManualConnect) {
// For manual attempts, set an error in the ConnectionManager to reflect in UI state.
bluetoothConnectionManager.setExternalConnectionError(errorMsg) bluetoothConnectionManager.setExternalConnectionError(errorMsg)
} else { } else {
// For automatic attempts (e.g., auto-connect), a Snackbar might be sufficient without altering permanent error state.
sharedViewModel.showSnackbar(errorMsg, SnackbarDuration.Long) sharedViewModel.showSnackbar(errorMsg, SnackbarDuration.Long)
} }
return false return false
} }
if (!isBluetoothEnabled()) { if (!isBluetoothEnabled()) {
val errorMsg = "Bluetooth is disabled. Please enable it to connect to ${deviceName ?: "the device"}." val errorMsg = application.getString(
LogManager.w(TAG, "Connection prerequisite failed for '${deviceName ?: "device"}': $errorMsg") R.string.bt_snackbar_bluetooth_disabled_to_connect,
deviceNameForMessage ?: devicePlaceholder
)
LogManager.w(TAG, "Connection prerequisite failed: $errorMsg")
if (isManualConnect) { if (isManualConnect) {
bluetoothConnectionManager.setExternalConnectionError(errorMsg) bluetoothConnectionManager.setExternalConnectionError(errorMsg)
} else { } else {
@@ -412,26 +303,14 @@ class BluetoothViewModel(
} }
return false return false
} }
// User ID check is now more nuanced and handled within BluetoothConnectionManager,
// as its necessity can be handler-specific.
// LogManager.d(TAG, "Connection prerequisites met for ${deviceName ?: "device"}.")
return true return true
} }
/**
* Requests the [BluetoothConnectionManager] to disconnect from the currently connected device.
*/
fun disconnectDevice() { fun disconnectDevice() {
LogManager.i(TAG, "User requested to disconnect device. Delegating to BluetoothConnectionManager.") LogManager.i(TAG, "User requested to disconnect device. Delegating to BluetoothConnectionManager.")
bluetoothConnectionManager.disconnect() bluetoothConnectionManager.disconnect()
} }
// --- Error Handling ---
/**
* Clears all error states managed by both the scanner and connection managers.
*/
fun clearAllErrors() { fun clearAllErrors() {
LogManager.d(TAG, "Clearing all scan and connection errors.") LogManager.d(TAG, "Clearing all scan and connection errors.")
bluetoothScannerManager.clearScanError() bluetoothScannerManager.clearScanError()
@@ -445,67 +324,61 @@ class BluetoothViewModel(
fun processUserInteraction(interactionType: UserInteractionType, feedbackData: Any) { fun processUserInteraction(interactionType: UserInteractionType, feedbackData: Any) {
viewModelScope.launch { viewModelScope.launch {
val currentAppUser = sharedViewModel.selectedUser.value val localCurrentAppUser = currentAppUser // Use local copy for thread safety check
if (currentAppUser == null || currentAppUser.id == 0) { if (localCurrentAppUser == null || localCurrentAppUser.id == 0) {
sharedViewModel.showSnackbar("Fehler: Kein App-Benutzer ausgewählt.") sharedViewModel.showSnackbar(
application.getString(R.string.bt_snackbar_error_no_app_user_selected),
SnackbarDuration.Short // Assuming short duration, adjust if needed
)
bluetoothConnectionManager.clearUserInteractionEvent() bluetoothConnectionManager.clearUserInteractionEvent()
return@launch return@launch
} }
val appUserId = currentAppUser.id val appUserId = localCurrentAppUser.id
clearPendingUserInteraction() // BluetoothConnectionManager now internally uses viewModelScope for its operations,
// so direct Handler passing might be less critical if its methods are suspend or use its own scope.
// If direct MainLooper operations are still needed within provideUserInteractionFeedback:
val uiHandler = Handler(Looper.getMainLooper()) val uiHandler = Handler(Looper.getMainLooper())
bluetoothConnectionManager.provideUserInteractionFeedback( bluetoothConnectionManager.provideUserInteractionFeedback(
interactionType, interactionType,
appUserId, appUserId,
feedbackData, feedbackData,
uiHandler uiHandler // Pass if strictly needed by the manager for immediate UI thread tasks
) )
sharedViewModel.showSnackbar("Benutzereingabe verarbeitet.", SnackbarDuration.Short) sharedViewModel.showSnackbar(
application.getString(R.string.bt_snackbar_user_input_processed),
SnackbarDuration.Short
)
clearPendingUserInteraction()
} }
} }
// --- Device Preferences ---
/**
* Saves the given scanned device as the preferred Bluetooth scale in user settings.
* @param device The [ScannedDeviceInfo] of the device to save.
*/
fun saveDeviceAsPreferred(device: ScannedDeviceInfo) { fun saveDeviceAsPreferred(device: ScannedDeviceInfo) {
viewModelScope.launch { viewModelScope.launch {
val nameToSave = device.name ?: "Unknown Scale" // Provide a default name if null. val nameToSave = device.name ?: application.getString(R.string.unknown_scale_name) // Default name from resources
LogManager.i(TAG, "User requested to save device as preferred: Name='${device.name}', Address='${device.address}'. Saving as '$nameToSave'.") LogManager.i(TAG, "User requested to save device as preferred: Name='${device.name}', Address='${device.address}'. Saving as '$nameToSave'.")
userSettingsRepository.saveBluetoothScale(device.address, nameToSave) userSettingsRepository.saveBluetoothScale(device.address, nameToSave)
sharedViewModel.showSnackbar("'$nameToSave' saved as preferred scale.", SnackbarDuration.Short) sharedViewModel.showSnackbar(
// The savedScaleAddress/Name flows will update automatically, triggering any observers. application.getString(R.string.bt_snackbar_scale_saved_as_preferred, nameToSave),
SnackbarDuration.Short
)
} }
} }
// --- Permissions and System State Methods ---
/**
* Checks if the necessary Bluetooth permissions are currently granted.
* Handles different permission sets for Android S (API 31) and above vs. older versions.
* @return `true` if permissions are granted, `false` otherwise.
*/
private fun checkInitialPermissions(): Boolean { private fun checkInitialPermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else { } else {
// For older Android versions (below S)
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
} }
} }
/**
* Refreshes the `permissionsGranted` StateFlow by re-checking the current permission status.
* Should be called when the app regains focus or when permissions might have changed.
*/
fun refreshPermissionsStatus() { fun refreshPermissionsStatus() {
val currentStatus = checkInitialPermissions() val currentStatus = checkInitialPermissions()
if (_permissionsGranted.value != currentStatus) { if (_permissionsGranted.value != currentStatus) {
@@ -514,26 +387,12 @@ class BluetoothViewModel(
} }
} }
/**
* Checks if the Bluetooth adapter is currently enabled on the device.
* @return `true` if Bluetooth is enabled, `false` otherwise.
*/
fun isBluetoothEnabled(): Boolean { fun isBluetoothEnabled(): Boolean {
val bluetoothManager = application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager? val bluetoothManager = application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
val isEnabled = bluetoothManager?.adapter?.isEnabled ?: false return bluetoothManager?.adapter?.isEnabled ?: false
// LogManager.v(TAG, "Bluetooth enabled status check: $isEnabled") // Potentially too verbose for frequent checks
return isEnabled
} }
// Logic for handling Bluetooth events directly, saving measurements, observing communicator, @SuppressLint("MissingPermission")
// and releasing communicator has been moved to BluetoothConnectionManager.
/**
* Attempts to automatically connect to the saved preferred Bluetooth scale, if one exists
* and the app is not already connected or connecting to it.
* This might be called on ViewModel initialization or when the app comes to the foreground.
*/
@SuppressLint("MissingPermission") // connectToSavedDevice handles permission checks.
fun attemptAutoConnectToSavedScale() { fun attemptAutoConnectToSavedScale() {
viewModelScope.launch { viewModelScope.launch {
val address = savedScaleAddress.value val address = savedScaleAddress.value
@@ -541,27 +400,21 @@ class BluetoothViewModel(
if (address != null && name != null) { if (address != null && name != null) {
LogManager.i(TAG, "Attempting auto-connect to saved scale: '$name' ($address).") LogManager.i(TAG, "Attempting auto-connect to saved scale: '$name' ($address).")
// Check if already connected or connecting to the target device.
if ((connectionStatus.value == ConnectionStatus.CONNECTED || connectionStatus.value == ConnectionStatus.CONNECTING) && if ((connectionStatus.value == ConnectionStatus.CONNECTED || connectionStatus.value == ConnectionStatus.CONNECTING) &&
connectedDeviceAddress.value == address connectedDeviceAddress.value == address
) { ) {
LogManager.d(TAG, "Auto-connect: Already connected or connecting to '$name' ($address). No action needed.") LogManager.d(TAG, "Auto-connect: Already connected or connecting to '$name' ($address). No action needed.")
return@launch return@launch
} }
// Delegate to the standard method for connecting to a saved device.
connectToSavedDevice() connectToSavedDevice()
} else { } else {
LogManager.d(TAG, "Auto-connect attempt: No saved scale found.") LogManager.d(TAG, "Auto-connect attempt: No saved scale found.")
// Optionally show a (non-blocking) snackbar if desired, though usually auto-attempts are silent on "not found"
// sharedViewModel.showSnackbar(application.getString(R.string.bt_snackbar_no_scale_saved), SnackbarDuration.Short)
} }
} }
} }
/**
* Called when the ViewModel is about to be destroyed.
* Ensures that resources used by Bluetooth managers are released (e.g., stopping scans,
* disconnecting devices, closing underlying Bluetooth resources).
*/
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
LogManager.i(TAG, "BluetoothViewModel onCleared. Releasing resources from managers.") LogManager.i(TAG, "BluetoothViewModel onCleared. Releasing resources from managers.")

View File

@@ -381,6 +381,27 @@
<string name="delete_db_successful">Gesamte Datenbank wurde gelöscht.</string> <string name="delete_db_successful">Gesamte Datenbank wurde gelöscht.</string>
<string name="delete_db_error">Fehler beim Löschen der gesamten Datenbank.</string> <string name="delete_db_error">Fehler beim Löschen der gesamten Datenbank.</string>
<!-- BluetoothViewModel Snackbar Messages -->
<string name="bt_snackbar_permissions_required_to_scan">Bluetooth-Berechtigungen werden zum Scannen von Geräten benötigt.</string>
<string name="bt_snackbar_bluetooth_disabled_to_scan">Bluetooth ist deaktiviert. Bitte aktiviere es, um nach Geräten zu scannen.</string>
<string name="bt_snackbar_permissions_required_to_connect">Bluetooth-Berechtigungen werden benötigt, um eine Verbindung zu %1$s herzustellen.</string>
<string name="bt_snackbar_permissions_required_to_connect_default">Bluetooth-Berechtigungen werden benötigt, um eine Verbindung zum Gerät herzustellen.</string>
<string name="bt_snackbar_bluetooth_disabled_to_connect">Bluetooth ist deaktiviert. Bitte aktiviere es, um eine Verbindung zu %1$s herzustellen.</string>
<string name="bt_snackbar_bluetooth_disabled_to_connect_default">Bluetooth ist deaktiviert. Bitte aktiviere es, um eine Verbindung zum Gerät herzustellen.</string>
<string name="bt_snackbar_scale_saved_as_preferred">%1$s als bevorzugte Waage gespeichert.</string>
<string name="bt_snackbar_saved_scale_no_longer_supported">Gespeicherte Waage \'%1$s\' wird nicht mehr unterstützt.</string>
<string name="bt_snackbar_no_scale_saved">Keine Bluetooth-Waage in den Einstellungen gespeichert.</string>
<string name="bt_snackbar_error_no_app_user_selected">Fehler: Kein App-Benutzer ausgewählt.</string>
<string name="bt_snackbar_user_input_processed">Benutzereingabe verarbeitet.</string>
<string name="bt_snackbar_operation_successful">Vorgang erfolgreich.</string>
<string name="bt_snackbar_operation_failed">Vorgang fehlgeschlagen. Bitte versuche es erneut.</string>
<string name="device_placeholder_name">das Gerät</string>
<string name="unknown_scale_name">Unbekannte Waage</string>
<!-- Bluetooth Waagen Nachrichten --> <!-- Bluetooth Waagen Nachrichten -->
<string name="bluetooth_scale_trisa_message_not_paired_instruction">Diese Waage wurde nicht gekoppelt!\n\nHalten Sie die Taste an der Unterseite der Waage gedrückt, um sie in den Kopplungsmodus zu versetzen, und verbinden Sie sich dann erneut, um das Gerätepasswort abzurufen.</string> <string name="bluetooth_scale_trisa_message_not_paired_instruction">Diese Waage wurde nicht gekoppelt!\n\nHalten Sie die Taste an der Unterseite der Waage gedrückt, um sie in den Kopplungsmodus zu versetzen, und verbinden Sie sich dann erneut, um das Gerätepasswort abzurufen.</string>
<string name="bluetooth_scale_trisa_success_pairing">Kopplung erfolgreich!\n\nVerbinden Sie sich erneut, um Messdaten abzurufen.</string> <string name="bluetooth_scale_trisa_success_pairing">Kopplung erfolgreich!\n\nVerbinden Sie sich erneut, um Messdaten abzurufen.</string>

View File

@@ -383,6 +383,27 @@
<string name="delete_db_successful">Entire database has been deleted.</string> <string name="delete_db_successful">Entire database has been deleted.</string>
<string name="delete_db_error">Error deleting entire database.</string> <string name="delete_db_error">Error deleting entire database.</string>
<!-- BluetoothViewModel Snackbar Messages -->
<string name="bt_snackbar_permissions_required_to_scan">Bluetooth permissions are required to scan for devices.</string>
<string name="bt_snackbar_bluetooth_disabled_to_scan">Bluetooth is disabled. Please enable it to scan for devices.</string>
<string name="bt_snackbar_permissions_required_to_connect">Bluetooth permissions are required to connect to %1$s.</string>
<string name="bt_snackbar_permissions_required_to_connect_default">Bluetooth permissions are required to connect to the device.</string>
<string name="bt_snackbar_bluetooth_disabled_to_connect">Bluetooth is disabled. Please enable it to connect to %1$s.</string>
<string name="bt_snackbar_bluetooth_disabled_to_connect_default">Bluetooth is disabled. Please enable it to connect to the device.</string>
<string name="bt_snackbar_scale_saved_as_preferred">%1$s saved as preferred scale.</string>
<string name="bt_snackbar_saved_scale_no_longer_supported">Saved scale \'%1$s\' is no longer supported.</string>
<string name="bt_snackbar_no_scale_saved">No Bluetooth scale saved in settings.</string>
<string name="bt_snackbar_error_no_app_user_selected">Error: No app user selected.</string>
<string name="bt_snackbar_user_input_processed">User input processed.</string>
<string name="bt_snackbar_operation_successful">Operation successful.</string>
<string name="bt_snackbar_operation_failed">Operation failed. Please try again.</string>
<string name="device_placeholder_name">the device</string>
<string name="unknown_scale_name">Unknown Scale</string>
<!-- Bluetooth Scales Messages --> <!-- Bluetooth Scales Messages -->
<string name="bluetooth_scale_trisa_message_not_paired_instruction">This scale has not been paired!\n\nHold the button on the bottom of the scale to switch it to pairing mode, and then reconnect to retrieve the device password.</string> <string name="bluetooth_scale_trisa_message_not_paired_instruction">This scale has not been paired!\n\nHold the button on the bottom of the scale to switch it to pairing mode, and then reconnect to retrieve the device password.</string>
<string name="bluetooth_scale_trisa_success_pairing">Pairing succeeded!\n\nReconnect to retrieve measurement data.</string> <string name="bluetooth_scale_trisa_success_pairing">Pairing succeeded!\n\nReconnect to retrieve measurement data.</string>