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