mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-31 20:11:58 +02:00
Implemented Bluetooth user interaction handling
This commit is contained in:
@@ -88,8 +88,8 @@ android {
|
||||
}
|
||||
|
||||
create("beta") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
initWith(getByName("debug"))
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
applicationIdSuffix = ".beta"
|
||||
versionNameSuffix = "-beta"
|
||||
manifestPlaceholders["appName"] = "openScale beta"
|
||||
|
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package com.health.openscale.core.bluetooth
|
||||
|
||||
import android.os.Handler
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@@ -26,6 +27,11 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
* Defines the events that can be emitted by a [ScaleCommunicator].
|
||||
*/
|
||||
sealed class BluetoothEvent {
|
||||
enum class UserInteractionType {
|
||||
CHOOSE_USER,
|
||||
ENTER_CONSENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when a connection to a device has been successfully established.
|
||||
* @param deviceName The name of the connected device.
|
||||
@@ -76,15 +82,14 @@ sealed class BluetoothEvent {
|
||||
* Event triggered when user interaction is required to select a user on the scale.
|
||||
* This is often used when a scale supports multiple users and the app needs to clarify
|
||||
* which app user corresponds to the scale user.
|
||||
* @param description A message describing why user selection is needed.
|
||||
* @param deviceIdentifier The identifier (e.g., MAC address) of the device requiring user selection.
|
||||
* @param userData Optional data associated with the event, potentially containing information about users on the scale.
|
||||
* @param data Optional data associated with the event, potentially containing information about users on the scale.
|
||||
* The exact type should be defined by the communicator implementation if more specific data is available.
|
||||
*/
|
||||
data class UserSelectionRequired(
|
||||
val description: String,
|
||||
data class UserInteractionRequired(
|
||||
val deviceIdentifier: String,
|
||||
val userData: Any? // Consider a more specific type if the structure of eventData is known.
|
||||
val data: Any?,
|
||||
val interactionType: UserInteractionType,
|
||||
) : BluetoothEvent()
|
||||
}
|
||||
|
||||
@@ -110,9 +115,8 @@ interface ScaleCommunicator {
|
||||
* Initiates a connection attempt to the device with the specified MAC address.
|
||||
* @param address The MAC address of the target device.
|
||||
* @param scaleUser The user to be selected or used on the scale (optional).
|
||||
* @param appUserId The ID of the user in the application (optional, can be used for context).
|
||||
*/
|
||||
fun connect(address: String, scaleUser: ScaleUser?, appUserId: Int?)
|
||||
fun connect(address: String, scaleUser: ScaleUser?)
|
||||
|
||||
/**
|
||||
* Disconnects the existing connection to the currently connected device.
|
||||
@@ -132,4 +136,14 @@ interface ScaleCommunicator {
|
||||
* @return A [SharedFlow] of [BluetoothEvent]s.
|
||||
*/
|
||||
fun getEventsFlow(): SharedFlow<BluetoothEvent>
|
||||
|
||||
/**
|
||||
* Processes feedback received from the user for a previously requested interaction.
|
||||
*/
|
||||
fun processUserInteractionFeedback(
|
||||
interactionType: BluetoothEvent.UserInteractionType,
|
||||
appUserId: Int,
|
||||
feedbackData: Any,
|
||||
uiHandler: Handler
|
||||
)
|
||||
}
|
||||
|
@@ -61,32 +61,4 @@ class DummyScaleHandler(private val driverName: String) : ScaleDeviceHandler {
|
||||
println("DummyScaleHandler: disconnect called")
|
||||
// Add actual disconnection logic here
|
||||
}
|
||||
|
||||
override suspend fun provideUserSelection(
|
||||
selectedUser: ScaleUserListItem,
|
||||
requestContext: Any?
|
||||
): Boolean {
|
||||
// Dummy implementation
|
||||
println("DummyScaleHandler: provideUserSelection called for user ${selectedUser.displayData}")
|
||||
return true // Or false based on logic
|
||||
}
|
||||
|
||||
override suspend fun provideUserConsent(
|
||||
consentType: String,
|
||||
consented: Boolean,
|
||||
details: Map<String, Any>?
|
||||
): Boolean {
|
||||
// Dummy implementation
|
||||
println("DummyScaleHandler: provideUserConsent called for $consentType, consented: $consented")
|
||||
return true // Or false
|
||||
}
|
||||
|
||||
override suspend fun provideUserAttributes(
|
||||
attributes: Map<String, Any>,
|
||||
scaleUserIdentifier: Any?
|
||||
): Boolean {
|
||||
// Dummy implementation
|
||||
println("DummyScaleHandler: provideUserAttributes called with $attributes")
|
||||
return true // Or false
|
||||
}
|
||||
}
|
@@ -28,8 +28,11 @@ import android.os.Looper
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent
|
||||
import com.health.openscale.core.bluetooth.ScaleCommunicator
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser
|
||||
import com.health.openscale.core.bluetooth.scalesJava.BluetoothCommunication
|
||||
import com.health.openscale.core.bluetooth.scalesJava.LegacyScaleAdapter
|
||||
import com.health.openscale.core.utils.LogManager
|
||||
import com.welie.blessed.BluetoothCentralManager
|
||||
import com.welie.blessed.BluetoothCentralManagerCallback
|
||||
@@ -73,6 +76,7 @@ object ScaleGattAttributes {
|
||||
class ModernScaleAdapter(
|
||||
private val context: Context
|
||||
) : ScaleCommunicator {
|
||||
private val TAG = "ModernScaleAdapter"
|
||||
|
||||
private companion object {
|
||||
const val TAG = "ModernScaleAdapter"
|
||||
@@ -89,6 +93,14 @@ class ModernScaleAdapter(
|
||||
|
||||
private val _eventsFlow = MutableSharedFlow<BluetoothEvent>(replay = 1, extraBufferCapacity = 5)
|
||||
override fun getEventsFlow(): SharedFlow<BluetoothEvent> = _eventsFlow.asSharedFlow()
|
||||
override fun processUserInteractionFeedback(
|
||||
interactionType: UserInteractionType,
|
||||
appUserId: Int,
|
||||
feedbackData: Any,
|
||||
uiHandler: Handler
|
||||
) {
|
||||
LogManager.w(TAG, "Error not implemented processUserInteractionFeedback received: Type=$interactionType, UserID=$appUserId, Data=$feedbackData", null)
|
||||
}
|
||||
|
||||
private val _isConnecting = MutableStateFlow(false)
|
||||
override val isConnecting: StateFlow<Boolean> = _isConnecting.asStateFlow()
|
||||
@@ -183,7 +195,7 @@ class ModernScaleAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun connect(address: String, scaleUser: ScaleUser?, appUserId: Int?) {
|
||||
override fun connect(address: String, scaleUser: ScaleUser?) {
|
||||
adapterScope.launch {
|
||||
if (!::central.isInitialized) {
|
||||
LogManager.e(TAG, "BluetoothCentralManager nicht initialisiert, wahrscheinlich aufgrund fehlender Berechtigungen.")
|
||||
@@ -209,9 +221,8 @@ class ModernScaleAdapter(
|
||||
_isConnected.value = false
|
||||
targetAddress = address
|
||||
currentScaleUser = scaleUser
|
||||
currentAppUserId = appUserId
|
||||
|
||||
LogManager.i(TAG, "Verbindungsversuch zu $address mit Benutzer: ${scaleUser?.id}, AppUserID: $appUserId")
|
||||
LogManager.i(TAG, "Verbindungsversuch zu $address mit Benutzer: ${scaleUser?.id}")
|
||||
|
||||
// Stoppe vorherige Scans, falls vorhanden
|
||||
central.stopScan()
|
||||
|
@@ -18,6 +18,8 @@
|
||||
package com.health.openscale.core.bluetooth.scales
|
||||
|
||||
import android.util.SparseArray
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
|
||||
import com.health.openscale.core.bluetooth.scalesJava.BluetoothCommunication
|
||||
import com.health.openscale.core.data.MeasurementTypeKey // Required for DeviceValue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.UUID
|
||||
@@ -134,40 +136,10 @@ sealed class ScaleDeviceEvent {
|
||||
val payload: Any? = null // For optionally more structured info data
|
||||
) : ScaleDeviceEvent()
|
||||
|
||||
/**
|
||||
* Emitted when the scale requires the user to be selected on the device.
|
||||
* The UI should present this list to the user. The response is provided via [ScaleDeviceHandler.provideUserSelection].
|
||||
* @param userList A list of [ScaleUserListItem] objects representing the user profiles available on the scale.
|
||||
* @param requestContext An optional context object that the handler can send to correlate the response later.
|
||||
*/
|
||||
data class UserSelectionRequired(val userList: List<ScaleUserListItem>, val requestContext: Any? = null) : ScaleDeviceEvent()
|
||||
|
||||
/**
|
||||
* Emitted when the scale requires user consent (e.g., in the app) to perform an action
|
||||
* (e.g., user profile synchronization, data assignment, registration).
|
||||
* The response is provided via [ScaleDeviceHandler.provideUserConsent].
|
||||
* @param consentType An identifier for the type of consent requested (handler-specific, e.g., "register_new_user").
|
||||
* @param messageToUser A user-readable message explaining the reason for the consent.
|
||||
* @param details Optional additional details or data relevant to the consent
|
||||
* (e.g., proposed scaleUserIndex if registering a new user: `mapOf("scaleUserIndexProposal" -> 3)`).
|
||||
*/
|
||||
data class UserConsentRequired(
|
||||
val consentType: String, // e.g., "register_new_user", "confirm_user_match"
|
||||
val messageToUser: String,
|
||||
val details: Map<String, Any>? = null // e.g., mapOf("appUserId" -> 1, "scaleUserIndexProposal" -> 3)
|
||||
) : ScaleDeviceEvent()
|
||||
|
||||
/**
|
||||
* Emitted when the handler needs specific attributes of the app user to interact with the scale
|
||||
* (e.g., to create or update a user on the scale).
|
||||
* The response is provided via [ScaleDeviceHandler.provideUserAttributes].
|
||||
* @param requestedAttributes A list of keys indicating which attributes are needed (e.g., "height", "birthdate", "gender").
|
||||
* Example: `listOf("height_cm", "birth_date_epoch_ms", "gender_string")`
|
||||
* @param scaleUserIdentifier The identifier of the scale user for whom the attributes are needed (if applicable).
|
||||
*/
|
||||
data class UserAttributesRequired(
|
||||
val requestedAttributes: List<String>, // e.g., listOf("height_cm", "birth_date_epoch_ms", "gender_string")
|
||||
val scaleUserIdentifier: Any? = null
|
||||
data class UserInteractionRequired(
|
||||
val interactionType: UserInteractionType,
|
||||
val data: Any? = null, // Daten vom Handler an die UI
|
||||
val requestContext: Any? = null
|
||||
) : ScaleDeviceEvent()
|
||||
}
|
||||
|
||||
@@ -235,37 +207,6 @@ interface ScaleDeviceHandler {
|
||||
*/
|
||||
suspend fun disconnect()
|
||||
|
||||
/**
|
||||
* Called by the application to provide the user's selection in response to a
|
||||
* [ScaleDeviceEvent.UserSelectionRequired] event.
|
||||
*
|
||||
* @param selectedUser The [ScaleUserListItem] object selected by the user.
|
||||
* @param requestContext The context that was sent with the original `UserSelectionRequired` event.
|
||||
* @return `true` if the selection was processed successfully, `false` otherwise.
|
||||
*/
|
||||
suspend fun provideUserSelection(selectedUser: ScaleUserListItem, requestContext: Any? = null): Boolean
|
||||
|
||||
/**
|
||||
* Called by the application to provide the user's consent (or denial) in response to a
|
||||
* [ScaleDeviceEvent.UserConsentRequired] event.
|
||||
*
|
||||
* @param consentType The type of consent, as specified in the original event.
|
||||
* @param consented `true` if the user consented, `false` otherwise.
|
||||
* @param details Additional details that were sent with the original `UserConsentRequired` event.
|
||||
* @return `true` if the consent was processed successfully, `false` otherwise.
|
||||
*/
|
||||
suspend fun provideUserConsent(consentType: String, consented: Boolean, details: Map<String, Any>? = null): Boolean
|
||||
|
||||
/**
|
||||
* Called by the application to provide the requested user attributes in response to a
|
||||
* [ScaleDeviceEvent.UserAttributesRequired] event.
|
||||
*
|
||||
* @param attributes A map of the provided attributes (keys as requested in the event).
|
||||
* @param scaleUserIdentifier The identifier of the scale user, as requested in the event.
|
||||
* @return `true` if the attributes were processed successfully, `false` otherwise.
|
||||
*/
|
||||
suspend fun provideUserAttributes(attributes: Map<String, Any>, scaleUserIdentifier: Any? = null): Boolean
|
||||
|
||||
/**
|
||||
* Sends a device-specific command to the scale.
|
||||
* This is an escape-hatch function for handler-specific actions not covered by
|
||||
|
@@ -28,13 +28,16 @@ import android.content.pm.PackageManager;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
|
||||
import com.health.openscale.R;
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType;
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement;
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser;
|
||||
import com.health.openscale.core.data.User;
|
||||
import com.health.openscale.core.utils.LogManager;
|
||||
import com.welie.blessed.BluetoothCentralManager;
|
||||
import com.welie.blessed.BluetoothCentralManagerCallback;
|
||||
@@ -45,10 +48,12 @@ import com.welie.blessed.GattStatus;
|
||||
import com.welie.blessed.HciStatus;
|
||||
import com.welie.blessed.WriteType;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class BluetoothCommunication {
|
||||
private final String TAG = "BluetoothCommunication";
|
||||
public enum BT_STATUS {
|
||||
RETRIEVE_SCALE_DATA,
|
||||
INIT_PROCESS,
|
||||
@@ -59,8 +64,7 @@ public abstract class BluetoothCommunication {
|
||||
NO_DEVICE_FOUND,
|
||||
UNEXPECTED_ERROR,
|
||||
SCALE_MESSAGE,
|
||||
CHOOSE_SCALE_USER,
|
||||
ENTER_SCALE_USER_CONSENT,
|
||||
USER_INTERACTION_REQUIRED
|
||||
}
|
||||
|
||||
private int stepNr;
|
||||
@@ -75,7 +79,9 @@ public abstract class BluetoothCommunication {
|
||||
private BluetoothPeripheral btPeripheral;
|
||||
|
||||
private ScaleUser selectedScaleUser;
|
||||
private int selectedScaleUserId;
|
||||
|
||||
private List<ScaleUser> cachedAppUserList;
|
||||
protected ScaleMeasurement cachedLastMeasurementForSelectedUser;
|
||||
|
||||
public BluetoothCommunication(Context context)
|
||||
{
|
||||
@@ -85,7 +91,6 @@ public abstract class BluetoothCommunication {
|
||||
this.stopped = false;
|
||||
this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper()));
|
||||
this.selectedScaleUser = new ScaleUser();
|
||||
this.selectedScaleUserId = 0;
|
||||
}
|
||||
|
||||
public void setSelectedScaleUser(ScaleUser user) {
|
||||
@@ -96,24 +101,60 @@ public abstract class BluetoothCommunication {
|
||||
return selectedScaleUser;
|
||||
}
|
||||
|
||||
public List<ScaleUser> getScaleUserList() { return null; } // TODO Not implemented
|
||||
|
||||
public void setSelectedScaleUserId(int userId) {
|
||||
selectedScaleUserId = userId;
|
||||
}
|
||||
public int getSelectedScaleUserId() {
|
||||
return selectedScaleUserId;
|
||||
public void setScaleUserList(List<ScaleUser> userList) {
|
||||
cachedAppUserList = userList;
|
||||
}
|
||||
|
||||
public void updateScaleUser(ScaleUser user) {} // TODO Not implemented
|
||||
public List<ScaleUser> getScaleUserList() { return cachedAppUserList; }
|
||||
|
||||
public int getAssignableUser(float weight) { return 0; } // TODO Not implemented
|
||||
public void setCachedLastMeasurementForSelectedUser(ScaleMeasurement measurement) {
|
||||
this.cachedLastMeasurementForSelectedUser = measurement;
|
||||
if (measurement != null) {
|
||||
LogManager.d(TAG, "Cached last measurement for selected user (ID: " + getSelectedScaleUser().getId() + ") set.");
|
||||
} else {
|
||||
LogManager.d(TAG, "Cached last measurement for selected user (ID: " + getSelectedScaleUser().getId() + ") cleared.");
|
||||
}
|
||||
}
|
||||
public ScaleMeasurement getLastScaleMeasurement(int userId) {
|
||||
if (getSelectedScaleUser().getId() == userId && this.cachedLastMeasurementForSelectedUser != null) {
|
||||
LogManager.d(TAG, "Returning cached last measurement for user ID: " + userId);
|
||||
return this.cachedLastMeasurementForSelectedUser;
|
||||
}
|
||||
if (getSelectedScaleUser().getId() != userId) {
|
||||
LogManager.w(TAG, "Requested last measurement for user ID " + userId +
|
||||
", but cached data is for selected user ID " + getSelectedScaleUser().getId() + ". Returning null.", null);
|
||||
} else { // cachedLastMeasurementForSelectedUser is null
|
||||
LogManager.d(TAG, "No cached last measurement available for user ID: " + userId + ". Returning null.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ScaleUser getScaleUser(int userId) { return null; } // TODO Not implemented
|
||||
protected void requestUserInteraction(UserInteractionType interactionType, Object data) {
|
||||
if (callbackBtHandler != null) {
|
||||
Message msg = callbackBtHandler.obtainMessage(BT_STATUS.USER_INTERACTION_REQUIRED.ordinal());
|
||||
|
||||
public ScaleMeasurement getLastScaleMeasurement(int userId) { return null; } // TODO Not implemented
|
||||
public BluetoothPeripheral getBtPeripheral() {
|
||||
return btPeripheral;
|
||||
Object[] payload = new Object[]{interactionType, data};
|
||||
msg.obj = payload;
|
||||
|
||||
LogManager.d(TAG, "Sending USER_INTERACTION_REQUIRED (" + interactionType + ") to handler. Data: " + (data != null ? data.toString() : "null"));
|
||||
msg.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes feedback received from the user in response to a requestUserInteraction call.
|
||||
* The specific implementation in subclasses will handle the data based on the original interaction type.
|
||||
*
|
||||
* @param interactionType The type of interaction this feedback corresponds to.
|
||||
* @param appUserId The ID of the application user this feedback is for.
|
||||
* @param feedbackData Data provided by the user (e.g., selected scaleUserIndex, entered consent code).
|
||||
* The type and structure depend on the interactionType.
|
||||
* For CHOOSE_USER, this would be the selected scaleUserIndex (Integer).
|
||||
* For ENTER_CONSENT, this would be the consent code (Integer).
|
||||
* @param uiHandler Handler for potential further UI updates or operations within the communicator.
|
||||
*/
|
||||
public void processUserInteractionFeedback(UserInteractionType interactionType, int appUserId, Object feedbackData, Handler uiHandler) {
|
||||
LogManager.d(TAG, "processUserInteractionFeedback for " + interactionType + " not implemented in base class. AppUserId: " + appUserId);
|
||||
}
|
||||
|
||||
protected boolean needReConnect() {
|
||||
@@ -172,27 +213,13 @@ public abstract class BluetoothCommunication {
|
||||
}
|
||||
}
|
||||
|
||||
protected void chooseScaleUserUi(Object userList) {
|
||||
if (callbackBtHandler != null) {
|
||||
callbackBtHandler.obtainMessage(
|
||||
BT_STATUS.CHOOSE_SCALE_USER.ordinal(), userList).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
protected void enterScaleUserConsentUi(int appScaleUserId, int scaleUserIndex) {
|
||||
if (callbackBtHandler != null) {
|
||||
callbackBtHandler.obtainMessage(
|
||||
BT_STATUS.ENTER_SCALE_USER_CONSENT.ordinal(), appScaleUserId, scaleUserIndex).sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to openScale user
|
||||
*
|
||||
* @param msg the string id to be send
|
||||
* @param value the value to be used
|
||||
*/
|
||||
protected void sendMessage(int msg, Object value) { // TODO implement in openScale 3.0
|
||||
protected void sendMessage(int msg, Object value) {
|
||||
if (callbackBtHandler != null) {
|
||||
callbackBtHandler.obtainMessage(
|
||||
BT_STATUS.SCALE_MESSAGE.ordinal(), msg, 0, value).sendToTarget();
|
||||
@@ -412,15 +439,6 @@ public abstract class BluetoothCommunication {
|
||||
disconnectHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
public void selectScaleUserIndexForAppUserId(int appUserId, int scaleUserIndex, Handler uiHandler) {
|
||||
LogManager.d("BluetoothCommunication","Set scale user index for app user id: Not implemented!");
|
||||
}
|
||||
|
||||
public void setScaleUserConsent(int appUserId, int scaleUserConsent, Handler uiHandler) {
|
||||
LogManager.d("BluetoothCommunication","Set scale user consent for app user id: Not implemented!");
|
||||
}
|
||||
|
||||
// +++
|
||||
public byte[] getScaleMacAddress() {
|
||||
String[] mac = btPeripheral.getAddress().split(":");
|
||||
byte[] macAddress = new byte[6];
|
||||
|
@@ -238,7 +238,7 @@ public class BluetoothMiScale extends BluetoothCommunication {
|
||||
prefs.edit().putInt("uniqueNumber", uniqueNumber).apply();
|
||||
}
|
||||
|
||||
int userId = getSelectedScaleUserId();
|
||||
int userId = getSelectedScaleUser().getId();
|
||||
|
||||
return uniqueNumber + userId;
|
||||
}
|
||||
|
@@ -232,7 +232,7 @@ public class BluetoothMiScale2 extends BluetoothCommunication {
|
||||
prefs.edit().putInt("uniqueNumber", uniqueNumber).apply();
|
||||
}
|
||||
|
||||
int userId = getSelectedScaleUserId();
|
||||
int userId = getSelectedScaleUser().getId();
|
||||
|
||||
return uniqueNumber + userId;
|
||||
}
|
||||
|
@@ -73,7 +73,7 @@ public class BluetoothOneByoneNew extends BluetoothCommunication{
|
||||
impedance = Converters.fromUnsignedInt16Be(data, 15);
|
||||
|
||||
ScaleMeasurement historicMeasurement = new ScaleMeasurement();
|
||||
int assignableUserId = getAssignableUser(weight);
|
||||
int assignableUserId = getSelectedScaleUser().getId(); // TODO old implementation was getAssignableUser(weight);
|
||||
if(assignableUserId == -1){
|
||||
LogManager.i(TAG, "Discarding historic measurement: no user found with intelligent user recognition");
|
||||
break;
|
||||
@@ -117,10 +117,9 @@ public class BluetoothOneByoneNew extends BluetoothCommunication{
|
||||
LogManager.e(TAG, "Discarding measurement population since invalid user", null);
|
||||
return;
|
||||
}
|
||||
ScaleUser user = getScaleUser(userId);
|
||||
ScaleUser user = getSelectedScaleUser();
|
||||
float cmHeight = Converters.fromCentimeter(user.getBodyHeight(), user.getMeasureUnit());
|
||||
OneByoneNewLib onebyoneLib = new OneByoneNewLib(getUserGender(user), user.getAge(), cmHeight, user.getActivityLevel().toInt());
|
||||
measurement.setUserId(userId);
|
||||
measurement.setWeight(weight);
|
||||
measurement.setDateTime(Calendar.getInstance().getTime());
|
||||
measurement.setFat(onebyoneLib.getBodyFatPercentage(weight, impedance));
|
||||
@@ -156,7 +155,7 @@ public class BluetoothOneByoneNew extends BluetoothCommunication{
|
||||
break;
|
||||
case 2:
|
||||
// After the measurement took place, we store the data and send back to the scale
|
||||
sendUsersHistory(getSelectedScaleUserId());
|
||||
sendUsersHistory(getSelectedScaleUser().getId());
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
|
@@ -58,7 +58,7 @@ public class BluetoothSanitasSBF72 extends BluetoothStandardWeightProfile {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterScaleUserConsentUi(int appScaleUserId, int scaleUserIndex) {
|
||||
protected void requestScaleUserConsent(int appScaleUserId, int scaleUserIndex) {
|
||||
//Requests the scale to display the pin for the user in it's display.
|
||||
//As parameter we need to send a pin-index to the custom user-list characteristic.
|
||||
//For user with index 1 the pin-index is 0x11, for user with index 2 it is 0x12 and so on.
|
||||
@@ -68,7 +68,7 @@ public class BluetoothSanitasSBF72 extends BluetoothStandardWeightProfile {
|
||||
writeBytes(SERVICE_SBF72_CUSTOM, CHARACTERISTIC_USER_LIST, parser.getValue());
|
||||
|
||||
//opens the input screen for the pin in the app
|
||||
super.enterScaleUserConsentUi(appScaleUserId, scaleUserIndex);
|
||||
super.requestScaleUserConsent(appScaleUserId, scaleUserIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -30,6 +30,7 @@ import android.preference.PreferenceManager;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.health.openscale.R;
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType;
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement;
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser;
|
||||
import com.health.openscale.core.data.ActivityLevel;
|
||||
@@ -295,7 +296,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
|
||||
resumeMachineState();
|
||||
} else if (value[2] == UDS_CP_RESP_USER_NOT_AUTHORIZED) {
|
||||
LogManager.e(TAG, "UDS_CP_CONSENT: Not authorized", null);
|
||||
enterScaleUserConsentUi(this.selectedUser.getId(), getUserScaleIndex(this.selectedUser.getId()));
|
||||
requestScaleUserConsent(selectedUser.getId(), getUserScaleIndex(selectedUser.getId()));
|
||||
}
|
||||
else {
|
||||
LogManager.e(TAG, "UDS_CP_CONSENT: unhandled, code: " + value[2], null);
|
||||
@@ -355,12 +356,6 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
|
||||
}
|
||||
|
||||
if (registerNewUser) {
|
||||
LogManager.d(TAG, String.format(prefix + "Setting initial weight for user %s to: %s and registerNewUser to false", userID,
|
||||
weightValue));
|
||||
if (selectedUser.getId() == userID) {
|
||||
this.selectedUser.setInitialWeight(weightValue);
|
||||
updateScaleUser(selectedUser);
|
||||
}
|
||||
registerNewUser = false;
|
||||
resumeMachineState();
|
||||
}
|
||||
@@ -721,32 +716,50 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
|
||||
resumeMachineState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void selectScaleUserIndexForAppUserId(int appUserId, int scaleUserIndex, Handler uiHandler) {
|
||||
LogManager.d(TAG, "Select scale user index from UI: user id: " + appUserId + ", scale user index: " + scaleUserIndex);
|
||||
if (scaleUserIndex == -1) {
|
||||
reconnectOrSetSmState(SM_STEPS.REGISTER_NEW_SCALE_USER, SM_STEPS.REGISTER_NEW_SCALE_USER, uiHandler);
|
||||
}
|
||||
else {
|
||||
storeUserScaleIndex(appUserId, scaleUserIndex);
|
||||
if (getUserScaleConsent(appUserId) == -1) {
|
||||
enterScaleUserConsentUi(appUserId, scaleUserIndex);
|
||||
}
|
||||
else {
|
||||
reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
}
|
||||
}
|
||||
protected void requestScaleUserConsent(int appScaleUserId, int scaleUserIndex) {
|
||||
Object[] consentRequestData = new Object[]{appScaleUserId, scaleUserIndex};
|
||||
requestUserInteraction(UserInteractionType.ENTER_CONSENT, consentRequestData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScaleUserConsent(int appUserId, int scaleUserConsent, Handler uiHandler) {
|
||||
LogManager.d(TAG, "set scale user consent from UI: user id: " + appUserId + ", scale user consent: " + scaleUserConsent);
|
||||
storeUserScaleConsentCode(appUserId, scaleUserConsent);
|
||||
if (scaleUserConsent == -1) {
|
||||
reconnectOrSetSmState(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
}
|
||||
else {
|
||||
reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
@Override
|
||||
public void processUserInteractionFeedback(UserInteractionType interactionType, int appUserId, Object feedbackData, Handler uiHandler) {
|
||||
LogManager.d(TAG, "Processing UserInteractionFeedback: " + interactionType + " for appUserId: " + appUserId);
|
||||
switch (interactionType) {
|
||||
case CHOOSE_USER:
|
||||
if (feedbackData instanceof Integer) {
|
||||
int scaleUserIndex = (Integer) feedbackData;
|
||||
LogManager.d(TAG, "CHOOSE_USER Feedback: scaleUserIndex = " + scaleUserIndex);
|
||||
if (scaleUserIndex == -1) { // User wants to create a new user
|
||||
reconnectOrSetSmState(SM_STEPS.REGISTER_NEW_SCALE_USER, SM_STEPS.REGISTER_NEW_SCALE_USER, uiHandler);
|
||||
} else { // User selected an existing scale user
|
||||
storeUserScaleIndex(appUserId, scaleUserIndex);
|
||||
if (getUserScaleConsent(appUserId) == -1) {
|
||||
requestScaleUserConsent(appUserId, scaleUserIndex);
|
||||
} else {
|
||||
reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LogManager.e(TAG, "CHOOSE_USER feedbackData is not an Integer: " + feedbackData, null);
|
||||
}
|
||||
break;
|
||||
case ENTER_CONSENT:
|
||||
if (feedbackData instanceof Integer) {
|
||||
int scaleUserConsent = (Integer) feedbackData;
|
||||
LogManager.d(TAG, "ENTER_CONSENT Feedback: scaleUserConsent = " + scaleUserConsent);
|
||||
storeUserScaleConsentCode(appUserId, scaleUserConsent);
|
||||
if (scaleUserConsent == -1) { // User cancelled or denied consent
|
||||
reconnectOrSetSmState(SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
} else { // User provided consent
|
||||
reconnectOrSetSmState(SM_STEPS.SELECT_SCALE_USER, SM_STEPS.REQUEST_VENDOR_SPECIFIC_USER_LIST, uiHandler);
|
||||
}
|
||||
} else {
|
||||
LogManager.e(TAG, "ENTER_CONSENT feedbackData is not an Integer: " + feedbackData, null);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LogManager.w(TAG, "Unhandled UserInteractionType in processUserInteractionFeedback: " + interactionType, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,7 +854,7 @@ public abstract class BluetoothStandardWeightProfile extends BluetoothCommunicat
|
||||
indexArray[userList.size()] = -1;
|
||||
}
|
||||
Pair<CharSequence[], int[]> choices = new Pair(choiceStrings, indexArray);
|
||||
chooseScaleUserUi(choices);
|
||||
requestUserInteraction(UserInteractionType.CHOOSE_USER, choices);
|
||||
}
|
||||
|
||||
protected String getInitials(String fullName) {
|
||||
|
@@ -184,7 +184,7 @@ public class BluetoothYunmaiSE_Mini extends BluetoothCommunication {
|
||||
prefs.edit().putInt("uniqueNumber", uniqueNumber).apply();
|
||||
}
|
||||
|
||||
int userId = getSelectedScaleUserId();
|
||||
int userId = getSelectedScaleUser().getId();
|
||||
|
||||
return uniqueNumber + userId;
|
||||
}
|
||||
|
@@ -17,15 +17,20 @@
|
||||
*/
|
||||
package com.health.openscale.core.bluetooth.scalesJava
|
||||
|
||||
import android.R.attr.description
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.core.graphics.values
|
||||
import com.health.openscale.R
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent
|
||||
import com.health.openscale.core.bluetooth.ScaleCommunicator
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser
|
||||
import com.health.openscale.core.data.MeasurementTypeKey
|
||||
import com.health.openscale.core.database.DatabaseRepository
|
||||
import com.health.openscale.core.utils.LogManager
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
@@ -40,8 +45,13 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
import kotlin.text.find
|
||||
import kotlin.text.toDouble
|
||||
|
||||
/**
|
||||
* Adapter that adapts a legacy `BluetoothCommunication` (Java driver) instance
|
||||
@@ -89,6 +99,75 @@ class LegacyScaleAdapter(
|
||||
bluetoothDriverInstance.registerCallbackHandler(driverEventHandler)
|
||||
}
|
||||
|
||||
private suspend fun provideUserDataToLegacyDriver() {
|
||||
try {
|
||||
LogManager.d(TAG, "Attempting to load user data for legacy driver: ${bluetoothDriverInstance.driverName()}")
|
||||
val userListFromDb = databaseRepository.getAllUsers().first()
|
||||
|
||||
val legacyScaleUserList = userListFromDb.map { kotlinUser ->
|
||||
ScaleUser().apply {
|
||||
setId(kotlinUser.id)
|
||||
setUserName(kotlinUser.name)
|
||||
setBirthday(Date(kotlinUser.birthDate))
|
||||
setBodyHeight(if (kotlinUser.heightCm == null) 0f else kotlinUser.heightCm)
|
||||
setGender(kotlinUser.gender)
|
||||
setActivityLevel(kotlinUser.activityLevel)
|
||||
|
||||
// TODO setInitialWeight(kotlinUser.initialWeight)
|
||||
// TODO setGoalWeight(kotlinUser.goalWeight)
|
||||
// TODO usw.
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothDriverInstance.setScaleUserList(legacyScaleUserList)
|
||||
|
||||
LogManager.i(TAG, "Successfully provided ${userListFromDb.size} users to legacy driver ${bluetoothDriverInstance.driverName()}.")
|
||||
|
||||
this.currentInternalUser?.let { userId ->
|
||||
if (userId.id != -1) {
|
||||
LogManager.d(TAG, "Attempting to load last measurement for user ID: $userId using existing repository methods.")
|
||||
val lastMeasurementWithValues: com.health.openscale.core.model.MeasurementWithValues? =
|
||||
databaseRepository.getMeasurementsWithValuesForUser(userId.id)
|
||||
.map { measurements ->
|
||||
measurements.maxByOrNull { it.measurement.timestamp }
|
||||
}
|
||||
.first()
|
||||
|
||||
if (lastMeasurementWithValues != null) {
|
||||
val legacyScaleMeasurement = ScaleMeasurement().apply {
|
||||
setDateTime(Date(lastMeasurementWithValues.measurement.timestamp))
|
||||
setUserId(lastMeasurementWithValues.measurement.userId)
|
||||
setWeight(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.WEIGHT }?.value?.floatValue ?: 0.0f)
|
||||
setFat(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.BODY_FAT }?.value?.floatValue ?: 0.0f)
|
||||
setWater(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.WATER }?.value?.floatValue ?: 0.0f)
|
||||
setMuscle(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.MUSCLE }?.value?.floatValue ?: 0.0f)
|
||||
setBone(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.BONE }?.value?.floatValue ?: 0.0f)
|
||||
setVisceralFat(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.VISCERAL_FAT }?.value?.floatValue ?: 0.0f)
|
||||
setLbm(lastMeasurementWithValues.values.find { it.type.key == MeasurementTypeKey.LBM }?.value?.floatValue ?: 0.0f)
|
||||
}
|
||||
bluetoothDriverInstance.setCachedLastMeasurementForSelectedUser(legacyScaleMeasurement)
|
||||
LogManager.i(TAG, "Successfully provided last measurement for user $userId to legacy driver.")
|
||||
} else {
|
||||
bluetoothDriverInstance.setCachedLastMeasurementForSelectedUser(null)
|
||||
LogManager.d(TAG, "No last measurement found for user $userId.")
|
||||
}
|
||||
} else {
|
||||
bluetoothDriverInstance.setCachedLastMeasurementForSelectedUser(null)
|
||||
}
|
||||
} ?: run {
|
||||
bluetoothDriverInstance.setCachedLastMeasurementForSelectedUser(null)
|
||||
LogManager.d(TAG, "No current app user ID set, cannot load last measurement.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogManager.e(TAG, "Error providing user data to legacy driver ${bluetoothDriverInstance.driverName()}", e)
|
||||
_eventsFlow.tryEmit(BluetoothEvent.DeviceMessage("Fehler beim Laden der Benutzerdaten für ${bluetoothDriverInstance.driverName()}",
|
||||
bluetoothDriverInstance.scaleMacAddress.toString()
|
||||
))
|
||||
bluetoothDriverInstance.setCachedLastMeasurementForSelectedUser(null)
|
||||
LogManager.d(TAG, "No current app user ID set, cannot load last measurement.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages received from the legacy [BluetoothCommunication] driver.
|
||||
* It translates these messages into [BluetoothEvent]s and updates the adapter's state.
|
||||
@@ -182,31 +261,29 @@ class LegacyScaleAdapter(
|
||||
adapter._eventsFlow.tryEmit(BluetoothEvent.DeviceMessage(fallbackMessage, deviceIdentifier))
|
||||
}
|
||||
}
|
||||
BluetoothCommunication.BT_STATUS.CHOOSE_SCALE_USER -> {
|
||||
LogManager.d(TAG, "CHOOSE_SCALE_USER for $deviceIdentifier: Data: $eventData")
|
||||
var userListDescription = applicationContext.getString(R.string.legacy_adapter_event_user_selection_required)
|
||||
if (eventData is List<*>) {
|
||||
val stringList = eventData.mapNotNull { item ->
|
||||
if (item is ScaleUser) {
|
||||
applicationContext.getString(R.string.legacy_adapter_event_user_details, item.id, item.age, item.bodyHeight)
|
||||
} else {
|
||||
item.toString()
|
||||
}
|
||||
}
|
||||
if (stringList.isNotEmpty()) {
|
||||
userListDescription = stringList.joinToString(separator = "\n")
|
||||
}
|
||||
} else if (eventData != null) {
|
||||
userListDescription = eventData.toString()
|
||||
BluetoothCommunication.BT_STATUS.USER_INTERACTION_REQUIRED -> {
|
||||
val rawPayload = msg.obj
|
||||
if (rawPayload is Array<*> && rawPayload.size == 2 && rawPayload[0] is UserInteractionType) {
|
||||
val interactionType = rawPayload[0] as UserInteractionType
|
||||
val eventDataForUi = rawPayload[1]
|
||||
|
||||
LogManager.d(TAG, "USER_INTERACTION_REQUIRED ($interactionType) for $deviceIdentifier. Forwarding data: $eventDataForUi")
|
||||
|
||||
adapter._eventsFlow.tryEmit(
|
||||
BluetoothEvent.UserInteractionRequired(
|
||||
deviceIdentifier = deviceIdentifier,
|
||||
data = eventDataForUi,
|
||||
interactionType = interactionType
|
||||
)
|
||||
)
|
||||
|
||||
} else {
|
||||
LogManager.w(TAG, "Received USER_INTERACTION_REQUIRED with invalid or incomplete payload structure: $rawPayload")
|
||||
adapter._eventsFlow.tryEmit(BluetoothEvent.DeviceMessage(
|
||||
applicationContext.getString(R.string.legacy_adapter_event_invalid_interaction_payload),
|
||||
deviceIdentifier
|
||||
))
|
||||
}
|
||||
adapter._eventsFlow.tryEmit(BluetoothEvent.UserSelectionRequired(userListDescription, deviceIdentifier, eventData))
|
||||
}
|
||||
BluetoothCommunication.BT_STATUS.ENTER_SCALE_USER_CONSENT -> {
|
||||
val appScaleUserId = arg1
|
||||
val scaleUserIndex = arg2
|
||||
LogManager.d(TAG, "ENTER_SCALE_USER_CONSENT for $deviceIdentifier: AppUserID: $appScaleUserId, ScaleUserIndex: $scaleUserIndex. Data: $eventData")
|
||||
val message = applicationContext.getString(R.string.legacy_adapter_event_user_consent_required, appScaleUserId, scaleUserIndex)
|
||||
adapter._eventsFlow.tryEmit(BluetoothEvent.DeviceMessage(message, deviceIdentifier))
|
||||
}
|
||||
else -> {
|
||||
LogManager.w(TAG, "Unknown BT_STATUS ($status) or message (what=${msg.what}) from driver ${adapter.bluetoothDriverInstance.driverName()} received.")
|
||||
@@ -219,7 +296,7 @@ class LegacyScaleAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
override fun connect(address: String, scaleUser: ScaleUser?, appUserId: Int?) {
|
||||
override fun connect(address: String, scaleUser: ScaleUser?) {
|
||||
adapterScope.launch {
|
||||
val currentDeviceName = currentTargetAddress ?: bluetoothDriverInstance.driverName()
|
||||
if (_isConnected.value || _isConnecting.value) {
|
||||
@@ -238,16 +315,17 @@ class LegacyScaleAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
LogManager.i(TAG, "connect: REQUEST for address $address to driver ${bluetoothDriverInstance.driverName()}, UI ScaleUser ID: ${scaleUser?.id}, AppUserID: $appUserId")
|
||||
LogManager.i(TAG, "connect: REQUEST for address $address to driver ${bluetoothDriverInstance.driverName()}")
|
||||
_isConnecting.value = true
|
||||
_isConnected.value = false
|
||||
currentTargetAddress = address // Store the address being connected to
|
||||
currentInternalUser = scaleUser
|
||||
|
||||
LogManager.d(TAG, "connect: Internal user for connection: ${currentInternalUser?.id}, AppUserID: $appUserId")
|
||||
LogManager.d(TAG, "connect: Internal user for connection: ${currentInternalUser?.id}")
|
||||
|
||||
provideUserDataToLegacyDriver()
|
||||
|
||||
currentInternalUser?.let { bluetoothDriverInstance.setSelectedScaleUser(it) }
|
||||
appUserId?.let { bluetoothDriverInstance.setSelectedScaleUserId(it) }
|
||||
|
||||
LogManager.d(TAG, "connect: Calling connect() on Java driver instance (${bluetoothDriverInstance.driverName()}) for $address.")
|
||||
try {
|
||||
@@ -330,34 +408,6 @@ class LegacyScaleAdapter(
|
||||
LogManager.i(TAG, "release: AdapterScope for $deviceName cancelled.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the legacy driver about the user's selection for a scale user.
|
||||
* This is typically called in response to a [BluetoothEvent.UserSelectionRequired] event.
|
||||
*
|
||||
* @param appUserId The application-specific user ID.
|
||||
* @param scaleUserIndex The index of the user on the scale.
|
||||
*/
|
||||
fun selectLegacyScaleUserIndex(appUserId: Int, scaleUserIndex: Int) {
|
||||
adapterScope.launch {
|
||||
LogManager.i(TAG, "selectLegacyScaleUserIndex for ${bluetoothDriverInstance.driverName()}: AppUserID: $appUserId, ScaleUserIndex: $scaleUserIndex")
|
||||
bluetoothDriverInstance.selectScaleUserIndexForAppUserId(appUserId, scaleUserIndex, driverEventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the user's consent value to the legacy scale driver.
|
||||
* This is typically called after the scale requests user consent.
|
||||
*
|
||||
* @param appUserId The application-specific user ID.
|
||||
* @param consentValue The consent value (specific to the driver's protocol).
|
||||
*/
|
||||
fun setLegacyScaleUserConsent(appUserId: Int, consentValue: Int) {
|
||||
adapterScope.launch {
|
||||
LogManager.i(TAG, "setLegacyScaleUserConsent for ${bluetoothDriverInstance.driverName()}: AppUserID: $appUserId, ConsentValue: $consentValue")
|
||||
bluetoothDriverInstance.setScaleUserConsent(appUserId, consentValue, driverEventHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the managed Bluetooth driver/device.
|
||||
* Can be used externally if the name is needed and only a reference to the adapter is available.
|
||||
@@ -376,4 +426,13 @@ class LegacyScaleAdapter(
|
||||
override fun getEventsFlow(): SharedFlow<BluetoothEvent> {
|
||||
return events
|
||||
}
|
||||
|
||||
override fun processUserInteractionFeedback(
|
||||
interactionType: UserInteractionType,
|
||||
appUserId: Int,
|
||||
feedbackData: Any,
|
||||
uiHandler: Handler
|
||||
) {
|
||||
bluetoothDriverInstance.processUserInteractionFeedback(interactionType, appUserId, feedbackData, uiHandler)
|
||||
}
|
||||
}
|
||||
|
@@ -34,14 +34,23 @@ import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.BluetoothSearching
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.BluetoothSearching
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.HowToReg
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -55,22 +64,28 @@ import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.NavigationDrawerItem
|
||||
import androidx.compose.material3.NavigationDrawerItemDefaults
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -79,6 +94,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavType
|
||||
@@ -89,6 +105,8 @@ import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.health.openscale.BuildConfig
|
||||
import com.health.openscale.R
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
|
||||
import com.health.openscale.core.bluetooth.scalesJava.BluetoothCommunication
|
||||
import com.health.openscale.core.data.User
|
||||
import com.health.openscale.ui.navigation.Routes.getIconForRoute
|
||||
import com.health.openscale.ui.screen.SharedViewModel
|
||||
@@ -219,6 +237,167 @@ fun AppNavigation(sharedViewModel: SharedViewModel) {
|
||||
sharedViewModel.setTopBarAction(null)
|
||||
}
|
||||
|
||||
val pendingInteractionEvent by bluetoothViewModel.pendingUserInteractionEvent.collectAsState()
|
||||
|
||||
pendingInteractionEvent?.let { interactionEvent ->
|
||||
val dialogTitle: String
|
||||
val dialogIcon: @Composable (() -> Unit)
|
||||
|
||||
var consentCodeInput by rememberSaveable { mutableStateOf("") }
|
||||
var selectedUserIndexState by rememberSaveable { mutableIntStateOf(Int.MIN_VALUE) }
|
||||
|
||||
when (interactionEvent.interactionType) {
|
||||
UserInteractionType.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)) }
|
||||
// Reset state when dialog becomes visible for CHOOSE_USER
|
||||
if (interactionEvent.interactionType == UserInteractionType.CHOOSE_USER) {
|
||||
selectedUserIndexState = Int.MIN_VALUE
|
||||
}
|
||||
}
|
||||
UserInteractionType.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)) }
|
||||
// Reset state when dialog becomes visible for ENTER_CONSENT
|
||||
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) {
|
||||
consentCodeInput = ""
|
||||
}
|
||||
}
|
||||
// else -> { /* Handle unknown types or provide defaults */ } // Optional
|
||||
}
|
||||
|
||||
val isConsentInputValid = remember(consentCodeInput) { // Recalculate only when consentCodeInput changes
|
||||
consentCodeInput.isNotEmpty() && consentCodeInput.all { it.isDigit() }
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) {
|
||||
bluetoothViewModel.processUserInteraction(interactionEvent.interactionType, -1) // -1 abort signal
|
||||
}
|
||||
bluetoothViewModel.clearPendingUserInteraction()
|
||||
},
|
||||
icon = dialogIcon,
|
||||
title = { Text(text = dialogTitle) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (interactionEvent.interactionType == UserInteractionType.CHOOSE_USER)
|
||||
R.string.dialog_bt_interaction_desc_choose_user_default
|
||||
else
|
||||
R.string.dialog_bt_interaction_desc_enter_consent_default
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
when (interactionEvent.interactionType) {
|
||||
UserInteractionType.CHOOSE_USER -> {
|
||||
val choicesData = interactionEvent.data
|
||||
// Wir erwarten Pair<Array<String>, IntArray> oder Pair<Array<CharSequence>, IntArray>
|
||||
if (choicesData is Pair<*, *> && choicesData.first is Array<*> && choicesData.second is IntArray) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val choices = choicesData as Pair<Array<CharSequence>, IntArray>
|
||||
val choiceDisplayNames = choices.first
|
||||
val choiceIndices = choices.second
|
||||
|
||||
if (choiceDisplayNames.isNotEmpty() && choiceDisplayNames.size == choiceIndices.size) {
|
||||
LazyColumn(modifier = Modifier.padding(top = 8.dp)) {
|
||||
itemsIndexed(choiceDisplayNames) { itemIndex, choiceName ->
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = (choiceIndices[itemIndex] == selectedUserIndexState),
|
||||
onClick = { selectedUserIndexState = choiceIndices[itemIndex] }
|
||||
)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = (choiceIndices[itemIndex] == selectedUserIndexState),
|
||||
onClick = { selectedUserIndexState = choiceIndices[itemIndex] }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = choiceName.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(stringResource(R.string.dialog_bt_error_loading_user_list_empty))
|
||||
}
|
||||
} else {
|
||||
Text(stringResource(R.string.dialog_bt_error_loading_user_list_format))
|
||||
}
|
||||
}
|
||||
UserInteractionType.ENTER_CONSENT -> {
|
||||
OutlinedTextField(
|
||||
value = consentCodeInput,
|
||||
onValueChange = { consentCodeInput = it.filter { char -> char.isDigit() } },
|
||||
label = { Text(stringResource(R.string.dialog_bt_label_consent_code)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
// else -> { /* Handle unknown types or provide defaults */ } // Optional
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val interactionType = interactionEvent.interactionType
|
||||
var feedbackData: Any? = null // Any, da der Typ variieren kann
|
||||
|
||||
when (interactionType) {
|
||||
UserInteractionType.CHOOSE_USER -> {
|
||||
if (selectedUserIndexState != Int.MIN_VALUE) {
|
||||
feedbackData = selectedUserIndexState // Ist ein Int
|
||||
} else {
|
||||
sharedViewModel.showSnackbar(R.string.dialog_bt_select_user_prompt)
|
||||
}
|
||||
}
|
||||
UserInteractionType.ENTER_CONSENT -> {
|
||||
if (isConsentInputValid) {
|
||||
feedbackData = consentCodeInput.toInt()
|
||||
} else {
|
||||
sharedViewModel.showSnackbar(R.string.dialog_bt_enter_valid_code_prompt)
|
||||
}
|
||||
}
|
||||
// else -> { /* Handle unknown types or provide defaults */ } // Optional
|
||||
}
|
||||
|
||||
if (feedbackData != null) {
|
||||
bluetoothViewModel.processUserInteraction(interactionType, feedbackData)
|
||||
}
|
||||
},
|
||||
enabled = when (interactionEvent.interactionType) {
|
||||
UserInteractionType.CHOOSE_USER -> selectedUserIndexState != Int.MIN_VALUE
|
||||
UserInteractionType.ENTER_CONSENT -> isConsentInputValid
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.confirm_button))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (interactionEvent.interactionType == UserInteractionType.ENTER_CONSENT) {
|
||||
bluetoothViewModel.processUserInteraction(interactionEvent.interactionType, -1) // -1 for abort signal
|
||||
} else {
|
||||
bluetoothViewModel.clearPendingUserInteraction()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.cancel_button))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
|
@@ -19,12 +19,15 @@ package com.health.openscale.ui.screen.bluetooth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent
|
||||
import com.health.openscale.core.bluetooth.ScaleCommunicator
|
||||
import com.health.openscale.core.bluetooth.ScaleFactory
|
||||
import com.health.openscale.core.bluetooth.BluetoothEvent.UserInteractionType
|
||||
import com.health.openscale.core.bluetooth.data.ScaleMeasurement
|
||||
import com.health.openscale.core.bluetooth.data.ScaleUser
|
||||
import com.health.openscale.core.bluetooth.scalesJava.BluetoothCommunication
|
||||
import com.health.openscale.core.data.Measurement
|
||||
import com.health.openscale.core.data.MeasurementTypeKey
|
||||
import com.health.openscale.core.data.MeasurementValue
|
||||
@@ -68,8 +71,6 @@ class BluetoothConnectionManager(
|
||||
private val databaseRepository: DatabaseRepository,
|
||||
private val sharedViewModel: SharedViewModel,
|
||||
private val getCurrentScaleUser: () -> ScaleUser?,
|
||||
private val getCurrentAppUserId: () -> Int,
|
||||
private val onUserSelectionRequired: (BluetoothEvent.UserSelectionRequired) -> Unit,
|
||||
private val onSavePreferredDevice: suspend (address: String, name: String) -> Unit
|
||||
) : AutoCloseable {
|
||||
|
||||
@@ -94,13 +95,8 @@ class BluetoothConnectionManager(
|
||||
/** Emits an error message if a connection or operational error occurs, null otherwise. */
|
||||
val connectionError: StateFlow<String?> = _connectionError.asStateFlow()
|
||||
|
||||
private val _showUserSelectionDialog = MutableStateFlow<BluetoothEvent.UserSelectionRequired?>(null)
|
||||
/**
|
||||
* Emits a [BluetoothEvent.UserSelectionRequired] event when the connected scale requires
|
||||
* user interaction (e.g., selecting a user profile on the scale).
|
||||
* The UI should observe this and display an appropriate dialog.
|
||||
*/
|
||||
val showUserSelectionDialog: StateFlow<BluetoothEvent.UserSelectionRequired?> = _showUserSelectionDialog.asStateFlow()
|
||||
private val _userInteractionRequiredEvent = MutableStateFlow<BluetoothEvent.UserInteractionRequired?>(null)
|
||||
val userInteractionRequiredEvent: StateFlow<BluetoothEvent.UserInteractionRequired?> = _userInteractionRequiredEvent.asStateFlow()
|
||||
|
||||
private var activeCommunicator: ScaleCommunicator? = null
|
||||
private var communicatorJob: Job? = null // Job for observing events from the activeCommunicator.
|
||||
@@ -121,16 +117,11 @@ class BluetoothConnectionManager(
|
||||
val deviceDisplayName = deviceInfo.name ?: deviceInfo.address
|
||||
LogManager.i(TAG, "Attempting to connect to $deviceDisplayName")
|
||||
|
||||
// Basic validation logic (adapted from ViewModel).
|
||||
// Permissions and Bluetooth status should be checked BEFORE calling this method
|
||||
// in the ViewModel, as the manager cannot display UI for it.
|
||||
// Here only a fundamental check.
|
||||
val currentAppUserId = getCurrentAppUserId()
|
||||
// Some legacy or specific openScale handlers might require a valid user.
|
||||
val needsUserCheck = deviceInfo.determinedHandlerDisplayName?.contains("legacy", ignoreCase = true) == true ||
|
||||
deviceInfo.determinedHandlerDisplayName?.startsWith("com.health.openscale") == true
|
||||
|
||||
if (needsUserCheck && currentAppUserId == 0) {
|
||||
if (needsUserCheck && getCurrentScaleUser()?.id == 0) {
|
||||
LogManager.e(TAG, "User ID is 0, which might be problematic for handler '${deviceInfo.determinedHandlerDisplayName}'. Connection ABORTED.")
|
||||
_connectionError.value = "No user selected. Connection to $deviceDisplayName not possible."
|
||||
_connectionStatus.value = ConnectionStatus.FAILED
|
||||
@@ -165,7 +156,7 @@ class BluetoothConnectionManager(
|
||||
|
||||
LogManager.i(TAG, "ActiveCommunicator successfully created: ${activeCommunicator!!.javaClass.simpleName}. Starting observation job...")
|
||||
observeActiveCommunicatorEvents(deviceInfo)
|
||||
activeCommunicator?.connect(deviceInfo.address, getCurrentScaleUser(), currentAppUserId)
|
||||
activeCommunicator?.connect(deviceInfo.address, getCurrentScaleUser())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,18 +284,37 @@ class BluetoothConnectionManager(
|
||||
// Consider setting status to FAILED if it's a critical error
|
||||
// that impacts/loses the connection.
|
||||
}
|
||||
is BluetoothEvent.UserSelectionRequired -> {
|
||||
LogManager.i(TAG, "Event: UserSelectionRequired for ${event.deviceIdentifier}. Description: ${event.description}.")
|
||||
_showUserSelectionDialog.value = event // For the ViewModel to observe and show a dialog.
|
||||
onUserSelectionRequired(event) // Direct callback to ViewModel if it needs to react immediately.
|
||||
sharedViewModel.showSnackbar(
|
||||
"Action required on $deviceDisplayName: ${event.description.take(50)}...",
|
||||
SnackbarDuration.Long
|
||||
)
|
||||
|
||||
is BluetoothEvent.UserInteractionRequired -> {
|
||||
val actualDeviceIdentifier = event.deviceIdentifier
|
||||
LogManager.i(TAG, "Event: UserInteractionRequired (${event.interactionType}) for $actualDeviceIdentifier. Data: ${event.data}")
|
||||
_userInteractionRequiredEvent.value = event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards the user's feedback from an interaction to the active [ScaleCommunicator].
|
||||
*
|
||||
* @param interactionType The type of interaction this feedback corresponds to.
|
||||
* @param appUserId The ID of the current application user.
|
||||
* @param feedbackData Data provided by the user.
|
||||
* @param uiHandler A [Handler] required by the underlying communicator.
|
||||
*/
|
||||
fun provideUserInteractionFeedback(
|
||||
interactionType: UserInteractionType,
|
||||
appUserId: Int,
|
||||
feedbackData: Any,
|
||||
uiHandler: Handler
|
||||
) {
|
||||
scope.launch {
|
||||
activeCommunicator?.let { comm ->
|
||||
LogManager.d(TAG, "Forwarding user interaction feedback to communicator: type=$interactionType, appUserId=$appUserId")
|
||||
comm.processUserInteractionFeedback(interactionType, appUserId, feedbackData, uiHandler)
|
||||
} ?: LogManager.w(TAG, "provideUserInteractionFeedback called but no active communicator.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a [ScaleMeasurement] received from a device to the database.
|
||||
* This involves creating a [Measurement] entity and associated [MeasurementValue]s.
|
||||
@@ -314,7 +324,7 @@ class BluetoothConnectionManager(
|
||||
* @param deviceName The name of the device.
|
||||
*/
|
||||
private suspend fun saveMeasurementFromEvent(measurementData: ScaleMeasurement, deviceAddress: String, deviceName: String) {
|
||||
val currentAppUserId = getCurrentAppUserId()
|
||||
val currentAppUserId = getCurrentScaleUser()?.id
|
||||
if (currentAppUserId == 0) {
|
||||
LogManager.e(TAG, "($deviceName): No App User ID to save measurement.")
|
||||
sharedViewModel.showSnackbar("Measurement from $deviceName cannot be assigned to a user.", SnackbarDuration.Long)
|
||||
@@ -326,7 +336,7 @@ class BluetoothConnectionManager(
|
||||
// potentially be moved entirely into a dedicated MeasurementRepository or similar service.
|
||||
scope.launch(Dispatchers.IO) { // Perform database operations on IO dispatcher.
|
||||
val newDbMeasurement = Measurement(
|
||||
userId = currentAppUserId,
|
||||
userId = currentAppUserId ?: 0,
|
||||
timestamp = measurementData.dateTime?.time ?: System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -488,6 +498,12 @@ class BluetoothConnectionManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUserInteractionEvent() {
|
||||
if (_userInteractionRequiredEvent.value != null) {
|
||||
_userInteractionRequiredEvent.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up resources when the BluetoothConnectionManager is no longer needed.
|
||||
* This typically involves disconnecting any active connection and releasing the communicator.
|
||||
|
@@ -24,6 +24,8 @@ import android.bluetooth.BluetoothManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.material3.SnackbarDuration // Keep if used directly, otherwise remove
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -31,8 +33,10 @@ import androidx.lifecycle.viewModelScope
|
||||
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.BluetoothEvent.UserInteractionType
|
||||
// ScaleMeasurement no longer needed directly here for saveMeasurementFromEvent
|
||||
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.utils.LogManager
|
||||
@@ -116,11 +120,6 @@ class BluetoothViewModel(
|
||||
databaseRepository = databaseRepository,
|
||||
sharedViewModel = sharedViewModel,
|
||||
getCurrentScaleUser = { currentBtScaleUser },
|
||||
getCurrentAppUserId = { currentAppUserId },
|
||||
onUserSelectionRequired = { event ->
|
||||
// Update internal state when ConnectionManager requires user selection.
|
||||
_showUserSelectionDialogFromManager.value = event
|
||||
},
|
||||
onSavePreferredDevice = { address, name ->
|
||||
// Save preferred device when ConnectionManager successfully connects and indicates to do so.
|
||||
// Snackbar for user feedback can be shown here or in ConnectionManager; here is fine.
|
||||
@@ -162,13 +161,8 @@ class BluetoothViewModel(
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null)
|
||||
|
||||
// --- UI Interaction for User Selection (triggered by ConnectionManager callback) ---
|
||||
private val _showUserSelectionDialogFromManager = MutableStateFlow<BluetoothEvent.UserSelectionRequired?>(null)
|
||||
/**
|
||||
* Emits a [BluetoothEvent.UserSelectionRequired] when the connected scale needs user interaction
|
||||
* (e.g., selecting a user profile on the scale). The UI should observe this to show a dialog.
|
||||
* Emits null when the dialog should be dismissed or is not needed.
|
||||
*/
|
||||
val showUserSelectionDialog: StateFlow<BluetoothEvent.UserSelectionRequired?> = _showUserSelectionDialogFromManager.asStateFlow()
|
||||
val pendingUserInteractionEvent: StateFlow<BluetoothEvent.UserInteractionRequired?> =
|
||||
bluetoothConnectionManager.userInteractionRequiredEvent
|
||||
|
||||
init {
|
||||
LogManager.i(TAG, "ViewModel initialized. Setting up user observation.")
|
||||
@@ -446,17 +440,34 @@ class BluetoothViewModel(
|
||||
bluetoothConnectionManager.clearConnectionError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the user selection dialog state. This should be called by the UI
|
||||
* after the user has made a selection or dismissed the dialog.
|
||||
*/
|
||||
fun clearUserSelectionDialog() {
|
||||
LogManager.d(TAG, "Clearing user selection dialog.")
|
||||
_showUserSelectionDialogFromManager.value = null
|
||||
// If BluetoothConnectionManager held its own state for this event (beyond the callback),
|
||||
// a method like `bluetoothConnectionManager.userSelectionActionCompleted()` might be called here.
|
||||
fun clearPendingUserInteraction() {
|
||||
LogManager.d(TAG, "Requesting to clear pending user interaction event.")
|
||||
bluetoothConnectionManager.clearUserInteractionEvent()
|
||||
}
|
||||
|
||||
fun processUserInteraction(interactionType: UserInteractionType, feedbackData: Any) {
|
||||
viewModelScope.launch {
|
||||
val currentAppUser = sharedViewModel.selectedUser.value
|
||||
if (currentAppUser == null || currentAppUser.id == 0) {
|
||||
sharedViewModel.showSnackbar("Fehler: Kein App-Benutzer ausgewählt.")
|
||||
bluetoothConnectionManager.clearUserInteractionEvent()
|
||||
return@launch
|
||||
}
|
||||
val appUserId = currentAppUser.id
|
||||
|
||||
clearPendingUserInteraction()
|
||||
val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
bluetoothConnectionManager.provideUserInteractionFeedback(
|
||||
interactionType,
|
||||
appUserId,
|
||||
feedbackData,
|
||||
uiHandler
|
||||
)
|
||||
|
||||
sharedViewModel.showSnackbar("Benutzereingabe verarbeitet.", SnackbarDuration.Short)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Device Preferences ---
|
||||
|
||||
|
@@ -8,6 +8,7 @@
|
||||
<!-- Generische UI-Elemente & Aktionen -->
|
||||
<string name="enable_button">Aktivieren</string>
|
||||
<string name="cancel_button">Abbrechen</string>
|
||||
<string name="confirm_button">Bestätigen</string>
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="open_link_content_description">Link öffnen</string>
|
||||
<string name="content_desc_warning_icon">Warnsymbol</string>
|
||||
@@ -217,6 +218,10 @@
|
||||
<string name="legacy_adapter_connect_exception">Fehler beim Starten des Verbindungsprozesses zu %1$s: %2$s</string>
|
||||
<string name="legacy_adapter_request_measurement_not_connected">Nicht verbunden für Messanforderung.</string>
|
||||
<string name="legacy_adapter_request_measurement_auto">Messung sollte automatisch starten (Legacy-Treiber).</string>
|
||||
<string name="legacy_adapter_event_missing_user_data">Fehler: Fehlende Benutzerdaten für Auswahl.</string>
|
||||
<string name="legacy_adapter_event_invalid_consent_data">Fehler: Ungültige Daten für Zustimmung empfangen.</string>
|
||||
<string name="legacy_adapter_event_missing_consent_data">Fehler: Fehlende Daten für Zustimmung erforderlich.</string>
|
||||
<string name="legacy_adapter_event_invalid_interaction_payload">Fehler: Ungültige Daten-Nutzlast für Benutzerinteraktion empfangen.</string>
|
||||
|
||||
<!-- Diagramme (LineChart) -->
|
||||
<string name="content_description_time_range_icon">Zeitraumsymbol</string>
|
||||
@@ -375,4 +380,19 @@
|
||||
|
||||
<string name="permission_read_write_data_description">Auslesen der openScale Daten, inkl. Benutzerinformationen und aller gespeicherten Messungen</string>
|
||||
<string name="permission_read_write_data_label">Lese- und Schreibzugriff auf openScale Daten</string>
|
||||
|
||||
<string name="dialog_bt_interaction_title_choose_user">Benutzer auf Waage auswählen</string>
|
||||
<string name="dialog_bt_interaction_desc_choose_user_default">Wählen Sie Ihren Benutzer aus der Liste oder erstellen Sie einen neuen.</string>
|
||||
<string name="dialog_bt_icon_desc_choose_user">Benutzerauswahl</string>
|
||||
|
||||
<string name="dialog_bt_interaction_title_enter_consent">Bestätigungscode eingeben</string>
|
||||
<string name="dialog_bt_interaction_desc_enter_consent_default">Bitte geben Sie den auf Ihrer Waage angezeigten Code ein.</string>
|
||||
<string name="dialog_bt_icon_desc_enter_consent">Code-Eingabe</string>
|
||||
|
||||
<string name="dialog_bt_label_consent_code">Bestätigungscode</string>
|
||||
|
||||
<string name="dialog_bt_error_loading_user_list_empty">Benutzerliste ist leer oder fehlerhaft.</string>
|
||||
<string name="dialog_bt_error_loading_user_list_format">Fehler: Benutzerdaten konnten nicht korrekt geladen werden.</string>
|
||||
<string name="dialog_bt_select_user_prompt">Bitte einen Benutzer auswählen.</string>
|
||||
<string name="dialog_bt_enter_valid_code_prompt">Bitte einen gültigen Code eingeben.</string>
|
||||
</resources>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<!-- Generic UI Elements & Actions -->
|
||||
<string name="enable_button">Enable</string>
|
||||
<string name="cancel_button">Cancel</string>
|
||||
<string name="confirm_button">Confirm</string>
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="open_link_content_description">Open link</string>
|
||||
<string name="content_desc_warning_icon">Warning Icon</string>
|
||||
@@ -218,6 +219,10 @@
|
||||
<string name="legacy_adapter_connect_exception">Error starting connection process to %1$s: %2$s</string>
|
||||
<string name="legacy_adapter_request_measurement_not_connected">Not connected for measurement request.</string>
|
||||
<string name="legacy_adapter_request_measurement_auto">Measurement should start automatically (Legacy driver).</string>
|
||||
<string name="legacy_adapter_event_missing_user_data">Error: Missing user data for selection.</string>
|
||||
<string name="legacy_adapter_event_invalid_consent_data">Error: Invalid data received for consent.</string>
|
||||
<string name="legacy_adapter_event_missing_consent_data">Error: Missing data required for consent.</string>
|
||||
<string name="legacy_adapter_event_invalid_interaction_payload">Error: Invalid data payload received for user interaction.</string>
|
||||
|
||||
<!-- Charts (LineChart) -->
|
||||
<string name="content_description_time_range_icon">Time range icon</string>
|
||||
@@ -377,4 +382,16 @@
|
||||
<string name="permission_read_write_data_description">read/write openScale data, including user information and all saved measurements</string>
|
||||
<string name="permission_read_write_data_label">Read and Write openScale data</string>
|
||||
|
||||
<string name="dialog_bt_interaction_title_choose_user">Select User on Scale</string>
|
||||
<string name="dialog_bt_interaction_desc_choose_user_default">Select your user from the list or create a new one.</string>
|
||||
<string name="dialog_bt_icon_desc_choose_user">User Selection</string>
|
||||
<string name="dialog_bt_interaction_title_enter_consent">Enter Confirmation Code</string>
|
||||
<string name="dialog_bt_interaction_desc_enter_consent_default">Please enter the code displayed on your scale.</string>
|
||||
<string name="dialog_bt_icon_desc_enter_consent">Code Entry</string>
|
||||
<string name="dialog_bt_label_consent_code">Confirmation Code</string>
|
||||
<string name="dialog_bt_error_loading_user_list_empty">User list is empty or corrupted.</string>
|
||||
<string name="dialog_bt_error_loading_user_list_format">Error: User data could not be loaded correctly.</string>
|
||||
<string name="dialog_bt_select_user_prompt">Please select a user.</string>
|
||||
<string name="dialog_bt_enter_valid_code_prompt">Please enter a valid code.</string>
|
||||
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user