mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-24 01:03:20 +02:00
Refactors Bluetooth permission handling and connection logic across several parts of the application
This commit is contained in:
@@ -78,10 +78,6 @@ class ModernScaleAdapter(
|
|||||||
) : ScaleCommunicator {
|
) : ScaleCommunicator {
|
||||||
private val TAG = "ModernScaleAdapter"
|
private val TAG = "ModernScaleAdapter"
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TAG = "ModernScaleAdapter"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val adapterScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val adapterScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
private val mainHandler = Handler(Looper.getMainLooper())
|
private val mainHandler = Handler(Looper.getMainLooper())
|
||||||
private lateinit var central: BluetoothCentralManager // Initialisiert in init
|
private lateinit var central: BluetoothCentralManager // Initialisiert in init
|
||||||
@@ -185,35 +181,53 @@ class ModernScaleAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun hasRequiredBluetoothPermissions(): Boolean {
|
private fun hasRequiredBluetoothPermissions(): Boolean {
|
||||||
val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val required = listOf(
|
||||||
listOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
} else {
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
listOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION)
|
)
|
||||||
}
|
return required.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
|
||||||
return requiredPermissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call this after runtime grant or lazily before first connect
|
||||||
|
private fun ensureCentralReady(): Boolean {
|
||||||
|
if (!::central.isInitialized) {
|
||||||
|
val hasScan = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.BLUETOOTH_SCAN
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
val hasConnect = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (!(hasScan || hasConnect)) return false // at least one check; we hard-fail later if CONNECT is missing
|
||||||
|
central = BluetoothCentralManager(context, bluetoothCentralManagerCallback, mainHandler)
|
||||||
|
LogManager.d(TAG, "BluetoothCentralManager initialized")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun connect(address: String, scaleUser: ScaleUser?) {
|
override fun connect(address: String, scaleUser: ScaleUser?) {
|
||||||
adapterScope.launch {
|
adapterScope.launch {
|
||||||
if (!::central.isInitialized) {
|
// Ensure central exists (may be created after runtime grant)
|
||||||
LogManager.e(TAG, "BluetoothCentralManager nicht initialisiert, wahrscheinlich aufgrund fehlender Berechtigungen.")
|
if (!ensureCentralReady()) {
|
||||||
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(address, "Bluetooth nicht initialisiert (Berechtigungen?)"))
|
_eventsFlow.tryEmit(
|
||||||
|
BluetoothEvent.ConnectionFailed(address, "Bluetooth permissions missing (SCAN/CONNECT)")
|
||||||
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore duplicate connects
|
||||||
if (_isConnecting.value || (_isConnected.value && targetAddress == address)) {
|
if (_isConnecting.value || (_isConnected.value && targetAddress == address)) {
|
||||||
LogManager.d(TAG, "Verbindungsanfrage für $address ignoriert: Bereits verbunden oder Verbindungsaufbau läuft.")
|
LogManager.d(TAG, "connect($address) ignored: already connecting/connected")
|
||||||
if (_isConnected.value && targetAddress == address) {
|
if (_isConnected.value && targetAddress == address) {
|
||||||
val deviceName = currentPeripheral?.name ?: "Unbekanntes Gerät"
|
val deviceName = currentPeripheral?.name ?: "Unknown"
|
||||||
_eventsFlow.tryEmit(BluetoothEvent.Connected(deviceName, address))
|
_eventsFlow.tryEmit(BluetoothEvent.Connected(deviceName, address))
|
||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switch target: tear down old connection attempt
|
||||||
if ((_isConnected.value || _isConnecting.value) && targetAddress != address) {
|
if ((_isConnected.value || _isConnecting.value) && targetAddress != address) {
|
||||||
LogManager.d(TAG, "Bestehende Verbindung/Versuch zu $targetAddress wird für neue Verbindung zu $address getrennt.")
|
LogManager.d(TAG, "Switching from $targetAddress to $address")
|
||||||
disconnectLogic()
|
disconnectLogic()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,26 +236,54 @@ class ModernScaleAdapter(
|
|||||||
targetAddress = address
|
targetAddress = address
|
||||||
currentScaleUser = scaleUser
|
currentScaleUser = scaleUser
|
||||||
|
|
||||||
LogManager.i(TAG, "Verbindungsversuch zu $address mit Benutzer: ${scaleUser?.id}")
|
LogManager.i(TAG, "Connecting to $address (user=${scaleUser?.id})")
|
||||||
|
|
||||||
// Stoppe vorherige Scans, falls vorhanden
|
// Stop any previous scans
|
||||||
central.stopScan()
|
runCatching { central.stopScan() }
|
||||||
|
|
||||||
|
val hasScan = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.BLUETOOTH_SCAN
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
val hasConnect = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
if (!hasConnect) {
|
||||||
|
// Without CONNECT we can't perform GATT ops reliably
|
||||||
|
LogManager.e(TAG, "Missing BLUETOOTH_CONNECT")
|
||||||
|
_eventsFlow.tryEmit(
|
||||||
|
BluetoothEvent.ConnectionFailed(address, "Missing BLUETOOTH_CONNECT permission")
|
||||||
|
)
|
||||||
|
_isConnecting.value = false
|
||||||
|
targetAddress = null
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Versuche, direkt ein Peripheral-Objekt zu bekommen, falls die Adresse bekannt ist.
|
if (hasScan) {
|
||||||
//Blessed erlaubt auch das Scannen nach Adresse, was oft robuster ist.
|
// Preferred path on 12+: short pre-scan by address
|
||||||
//central.getPeripheral(address) ist eine Option, aber scanForPeripheralsWithAddresses ist oft besser.
|
central.scanForPeripheralsWithAddresses(arrayOf(address))
|
||||||
central.scanForPeripheralsWithAddresses(arrayOf(address))
|
LogManager.d(TAG, "Pre-scan started for $address")
|
||||||
LogManager.d(TAG, "Scan gestartet für Adresse: $address")
|
// onDiscoveredPeripheral will stop scan and connect
|
||||||
|
} else {
|
||||||
|
// Fallback: connect without pre-scan (may be less reliable on some OEMs)
|
||||||
|
LogManager.w(TAG, "BLUETOOTH_SCAN not granted → connecting without pre-scan")
|
||||||
|
val p = central.getPeripheral(address)
|
||||||
|
central.connectPeripheral(p, peripheralCallback)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
LogManager.e(TAG, "Fehler beim Starten des Scans für $address", e)
|
LogManager.e(TAG, "Failed to start connect/scan for $address", e)
|
||||||
_eventsFlow.tryEmit(BluetoothEvent.ConnectionFailed(address, "Scan konnte nicht gestartet werden: ${e.message}"))
|
_eventsFlow.tryEmit(
|
||||||
|
BluetoothEvent.ConnectionFailed(address, "Failed to start scan/connect: ${e.message}")
|
||||||
|
)
|
||||||
_isConnecting.value = false
|
_isConnecting.value = false
|
||||||
targetAddress = null
|
targetAddress = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val peripheralCallback: BluetoothPeripheralCallback =
|
private val peripheralCallback: BluetoothPeripheralCallback =
|
||||||
object : BluetoothPeripheralCallback() {
|
object : BluetoothPeripheralCallback() {
|
||||||
override fun onServicesDiscovered(peripheral: BluetoothPeripheral) {
|
override fun onServicesDiscovered(peripheral: BluetoothPeripheral) {
|
||||||
|
@@ -17,14 +17,11 @@
|
|||||||
|
|
||||||
package com.health.openscale.core.bluetooth.scalesJava;
|
package com.health.openscale.core.bluetooth.scalesJava;
|
||||||
|
|
||||||
import static android.content.Context.LOCATION_SERVICE;
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.bluetooth.le.ScanRecord;
|
import android.bluetooth.le.ScanRecord;
|
||||||
import android.bluetooth.le.ScanResult;
|
import android.bluetooth.le.ScanResult;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.LocationManager;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
@@ -136,19 +133,25 @@ public class BluetoothBroadcastScale extends BluetoothCommunication {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connect(String macAddress) {
|
public void connect(String macAddress) {
|
||||||
|
// Android 12+ (minSdk=31): scanning requires BLUETOOTH_SCAN, no location needed.
|
||||||
|
boolean canScan = ContextCompat.checkSelfPermission(
|
||||||
|
context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED;
|
||||||
|
|
||||||
LocationManager locationManager = (LocationManager)context.getSystemService(LOCATION_SERVICE);
|
if (!canScan) {
|
||||||
|
LogManager.e(TAG, "Missing BLUETOOTH_SCAN → cannot start LE scan for broadcast data.", null);
|
||||||
if ((ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) ||
|
setBluetoothStatus(BT_STATUS.UNEXPECTED_ERROR);
|
||||||
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED ) &&
|
// choose a better string if you have one, this is just a visible hint:
|
||||||
(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
sendMessage(com.health.openscale.R.string.info_bluetooth_connection_error_scale_offline, 0);
|
||||||
(locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)))
|
return;
|
||||||
) {
|
|
||||||
LogManager.d(TAG, "Do LE scan before connecting to device");
|
|
||||||
central.scanForPeripheralsWithAddresses(new String[]{macAddress});
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
LogManager.e(TAG,"No location permission, can't do anything", null);
|
try {
|
||||||
|
LogManager.d(TAG, "Starting LE scan for broadcast scale (no location required)");
|
||||||
|
// We scan by address; when advertising is seen, onDiscoveredPeripheral parses manufacturer data.
|
||||||
|
central.scanForPeripheralsWithAddresses(new String[]{ macAddress });
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogManager.e(TAG, "Failed to start LE scan: " + e.getMessage(), e);
|
||||||
|
setBluetoothStatus(BT_STATUS.UNEXPECTED_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,14 +18,12 @@
|
|||||||
package com.health.openscale.core.bluetooth.scalesJava;
|
package com.health.openscale.core.bluetooth.scalesJava;
|
||||||
|
|
||||||
import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
|
import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
|
||||||
import static android.content.Context.LOCATION_SERVICE;
|
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.bluetooth.BluetoothGattCharacteristic;
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
import android.bluetooth.le.ScanResult;
|
import android.bluetooth.le.ScanResult;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.location.LocationManager;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
@@ -88,7 +86,7 @@ public abstract class BluetoothCommunication {
|
|||||||
public BluetoothCommunication(Context context)
|
public BluetoothCommunication(Context context)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.disconnectHandler = new Handler();
|
this.disconnectHandler = new Handler(Looper.getMainLooper());
|
||||||
this.stepNr = 0;
|
this.stepNr = 0;
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper()));
|
this.central = new BluetoothCentralManager(context, bluetoothCentralCallback, new Handler(Looper.getMainLooper()));
|
||||||
@@ -609,28 +607,50 @@ public abstract class BluetoothCommunication {
|
|||||||
* @param macAddress the Bluetooth address to connect to
|
* @param macAddress the Bluetooth address to connect to
|
||||||
*/
|
*/
|
||||||
public void connect(String macAddress) {
|
public void connect(String macAddress) {
|
||||||
// Running an LE scan during connect improves connectivity on some phones
|
// Android 12+ (API 31+): SCAN needed for scanning, CONNECT needed for connect/GATT.
|
||||||
// (e.g. Sony Xperia Z5 compact, Android 7.1.1). For some scales (e.g. Medisana BS444)
|
final boolean isSPlus = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S;
|
||||||
// it seems to be a requirement that the scale is discovered before connecting to it.
|
|
||||||
// Otherwise the connection almost never succeeds.
|
|
||||||
LocationManager locationManager = (LocationManager)context.getSystemService(LOCATION_SERVICE);
|
|
||||||
|
|
||||||
if ((ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) ||
|
if (isSPlus) {
|
||||||
(ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED ) &&
|
boolean canConnect = ContextCompat.checkSelfPermission(
|
||||||
(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
|
context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED;
|
||||||
(locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)))
|
boolean canScan = ContextCompat.checkSelfPermission(
|
||||||
) {
|
context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED;
|
||||||
LogManager.d("BluetoothCommunication","Do LE scan before connecting to device");
|
|
||||||
central.scanForPeripheralsWithAddresses(new String[]{macAddress});
|
if (!canConnect) {
|
||||||
stopMachineState();
|
LogManager.e("BluetoothCommunication",
|
||||||
}
|
"API≥31: Missing BLUETOOTH_CONNECT → cannot connect/GATT. Aborting.", null);
|
||||||
else {
|
setBluetoothStatus(BT_STATUS.UNEXPECTED_ERROR);
|
||||||
LogManager.d("BluetoothCommunication","No location permission, connecting without LE scan");
|
sendMessage(R.string.info_bluetooth_connection_error_scale_offline, 0);
|
||||||
BluetoothPeripheral peripheral = central.getPeripheral(macAddress);
|
return;
|
||||||
connectToDevice(peripheral);
|
}
|
||||||
|
|
||||||
|
// Pre-scan improves connect reliability on some devices/scales
|
||||||
|
if (canScan) {
|
||||||
|
LogManager.d("BluetoothCommunication",
|
||||||
|
"API≥31: Do LE scan before connecting (no location needed)");
|
||||||
|
central.scanForPeripheralsWithAddresses(new String[]{macAddress});
|
||||||
|
stopMachineState(); // wait for onDiscoveredPeripheral → connect
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
LogManager.w("BluetoothCommunication",
|
||||||
|
"API≥31: BLUETOOTH_SCAN not granted → connecting without pre-scan (may be less reliable)", null);
|
||||||
|
BluetoothPeripheral peripheral = central.getPeripheral(macAddress);
|
||||||
|
try {
|
||||||
|
connectToDevice(peripheral);
|
||||||
|
} catch (SecurityException se) {
|
||||||
|
LogManager.e("BluetoothCommunication",
|
||||||
|
"SecurityException during connect (missing CONNECT?): " + se.getMessage(), se);
|
||||||
|
setBluetoothStatus(BT_STATUS.UNEXPECTED_ERROR);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defensive fallback (won't run with minSdk=31, but good for clarity)
|
||||||
|
LogManager.w("BluetoothCommunication","connect() called on API<31 path; no legacy handling active.", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void connectToDevice(BluetoothPeripheral peripheral) {
|
private void connectToDevice(BluetoothPeripheral peripheral) {
|
||||||
|
|
||||||
Handler handler = new Handler();
|
Handler handler = new Handler();
|
||||||
|
@@ -23,7 +23,6 @@ import android.app.Application
|
|||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
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
|
import androidx.compose.material3.SnackbarDuration
|
||||||
@@ -280,36 +279,6 @@ class BluetoothViewModel(
|
|||||||
|
|
||||||
// --- Connection Control ---
|
// --- Connection Control ---
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
val deviceDisplayName = deviceInfo.name ?: deviceInfo.address
|
|
||||||
LogManager.i(TAG, "User requested to connect to device: $deviceDisplayName")
|
|
||||||
|
|
||||||
if (isScanning.value) {
|
|
||||||
LogManager.d(TAG, "Scan is active, stopping it before initiating connection to $deviceDisplayName.")
|
|
||||||
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)) {
|
|
||||||
// validateConnectionPrerequisites logs and shows Snackbar/sets error for ConnectionManager.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
LogManager.d(TAG, "Prerequisites for connecting to $deviceDisplayName met. Delegating to BluetoothConnectionManager.")
|
|
||||||
bluetoothConnectionManager.connectToDevice(deviceInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to connect to the saved preferred Bluetooth scale.
|
* Attempts to connect to the saved preferred Bluetooth scale.
|
||||||
* Retrieves device info from [userSettingsRepository] and then delegates
|
* Retrieves device info from [userSettingsRepository] and then delegates
|
||||||
@@ -511,20 +480,34 @@ class BluetoothViewModel(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the necessary Bluetooth permissions are currently granted.
|
* Checks if the necessary Bluetooth permissions are currently granted.
|
||||||
* Handles different permission sets for Android S (API 31) and above vs. older versions.
|
* Handles different permission sets for Android S (API 31) and above.
|
||||||
* @return `true` if permissions are granted, `false` otherwise.
|
* @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) {
|
val hasConnect = ContextCompat.checkSelfPermission(
|
||||||
// Android 12 (API 31) and above require BLUETOOTH_SCAN and BLUETOOTH_CONNECT
|
application, Manifest.permission.BLUETOOTH_CONNECT
|
||||||
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
|
|
||||||
} else {
|
val hasScan = ContextCompat.checkSelfPermission(
|
||||||
// For older Android versions (below S / API 31)
|
application, Manifest.permission.BLUETOOTH_SCAN
|
||||||
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
ContextCompat.checkSelfPermission(application, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED &&
|
|
||||||
ContextCompat.checkSelfPermission(application, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
when {
|
||||||
|
!hasConnect && !hasScan -> {
|
||||||
|
LogManager.w(TAG, "Missing permissions: BLUETOOTH_CONNECT & BLUETOOTH_SCAN → BLE disabled (no connect, no scan).")
|
||||||
|
}
|
||||||
|
!hasConnect -> {
|
||||||
|
LogManager.w(TAG, "Missing permission: BLUETOOTH_CONNECT → cannot perform GATT ops (connect/read/write).")
|
||||||
|
}
|
||||||
|
!hasScan -> {
|
||||||
|
LogManager.w(TAG, "Missing permission: BLUETOOTH_SCAN → cannot scan/pre-scan (no discovery, less reliable connect).")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LogManager.d(TAG, "All required Bluetooth permissions granted (SCAN & CONNECT).")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return hasConnect && hasScan
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -17,8 +17,16 @@
|
|||||||
*/
|
*/
|
||||||
package com.health.openscale.ui.screen.overview
|
package com.health.openscale.ui.screen.overview
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -137,31 +145,47 @@ fun determineBluetoothTopBarAction(
|
|||||||
currentNavController: NavController,
|
currentNavController: NavController,
|
||||||
bluetoothViewModel: BluetoothViewModel,
|
bluetoothViewModel: BluetoothViewModel,
|
||||||
sharedViewModel: SharedViewModel,
|
sharedViewModel: SharedViewModel,
|
||||||
currentDeviceName: String?
|
currentDeviceName: String?,
|
||||||
|
permissionsLauncher: ManagedActivityResultLauncher<Array<String>, Map<String, @JvmSuppressWildcards Boolean>>,
|
||||||
|
enableBluetoothLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>
|
||||||
): SharedViewModel.TopBarAction? {
|
): SharedViewModel.TopBarAction? {
|
||||||
// Logic to determine if a connection or disconnection process is currently active
|
|
||||||
val btConnectingOrDisconnecting = savedAddr != null &&
|
|
||||||
(connStatusEnum == ConnectionStatus.CONNECTING || connStatusEnum == ConnectionStatus.DISCONNECTING) &&
|
|
||||||
// When connecting, connectedDevice might be null or the address being connected to.
|
|
||||||
// When disconnecting, connectedDevice should be the address of the device being disconnected.
|
|
||||||
(connectedDevice == savedAddr || connStatusEnum == ConnectionStatus.CONNECTING || (connStatusEnum == ConnectionStatus.DISCONNECTING && connectedDevice == savedAddr))
|
|
||||||
|
|
||||||
val deviceNameForMessage = currentDeviceName ?: context.getString(R.string.fallback_device_name_saved_scale)
|
val deviceNameForMessage = currentDeviceName ?: context.getString(R.string.fallback_device_name_saved_scale)
|
||||||
|
|
||||||
|
// Busy while connecting/disconnecting to the currently saved device
|
||||||
|
val isBusy = savedAddr != null &&
|
||||||
|
(connStatusEnum == ConnectionStatus.CONNECTING || connStatusEnum == ConnectionStatus.DISCONNECTING) &&
|
||||||
|
(connectedDevice == savedAddr || connStatusEnum == ConnectionStatus.CONNECTING ||
|
||||||
|
(connStatusEnum == ConnectionStatus.DISCONNECTING && connectedDevice == savedAddr))
|
||||||
|
|
||||||
|
// Helper to request enabling Bluetooth (actual connect is done in onActivityResult when OK)
|
||||||
|
val requestEnableBluetooth = {
|
||||||
|
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
enableBluetoothLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to request runtime permissions (actual connect is done in onResult when granted)
|
||||||
|
val requestBtPermissions = {
|
||||||
|
permissionsLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
// Case 1: Connection or disconnection process is actively running
|
// 1) Show non-interactive feedback while a user-initiated operation is ongoing
|
||||||
btConnectingOrDisconnecting -> SharedViewModel.TopBarAction(
|
isBusy -> SharedViewModel.TopBarAction(
|
||||||
icon = Icons.AutoMirrored.Filled.BluetoothSearching, // Icon for "searching" or "working"
|
icon = Icons.AutoMirrored.Filled.BluetoothSearching,
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_connecting_disconnecting_desc),
|
contentDescription = context.getString(R.string.bluetooth_action_connecting_disconnecting_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
// Typically, the button is not interactive during this time,
|
|
||||||
// but a Snackbar can confirm the ongoing process.
|
|
||||||
sharedViewModel.showSnackbar(
|
sharedViewModel.showSnackbar(
|
||||||
message = context.getString(
|
message = context.getString(
|
||||||
when (connStatusEnum) {
|
when (connStatusEnum) {
|
||||||
ConnectionStatus.CONNECTING -> R.string.snackbar_bluetooth_connecting_to
|
ConnectionStatus.CONNECTING -> R.string.snackbar_bluetooth_connecting_to
|
||||||
ConnectionStatus.DISCONNECTING -> R.string.snackbar_bluetooth_disconnecting_from
|
ConnectionStatus.DISCONNECTING -> R.string.snackbar_bluetooth_disconnecting_from
|
||||||
else -> R.string.snackbar_bluetooth_processing_with // Fallback
|
else -> R.string.snackbar_bluetooth_processing_with
|
||||||
},
|
},
|
||||||
deviceNameForMessage
|
deviceNameForMessage
|
||||||
),
|
),
|
||||||
@@ -170,9 +194,9 @@ fun determineBluetoothTopBarAction(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Case 2: No Bluetooth scale is saved
|
// 2) No saved device → navigate to Bluetooth settings
|
||||||
savedAddr == null -> SharedViewModel.TopBarAction(
|
savedAddr == null -> SharedViewModel.TopBarAction(
|
||||||
icon = Icons.Default.Bluetooth, // Default Bluetooth icon
|
icon = Icons.Default.Bluetooth,
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_no_scale_saved_desc),
|
contentDescription = context.getString(R.string.bluetooth_action_no_scale_saved_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
sharedViewModel.showSnackbar(
|
sharedViewModel.showSnackbar(
|
||||||
@@ -183,52 +207,40 @@ fun determineBluetoothTopBarAction(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Case 3: Successfully connected to the saved scale
|
// 3) Connected → offer disconnect
|
||||||
savedAddr == connectedDevice && connStatusEnum == ConnectionStatus.CONNECTED -> SharedViewModel.TopBarAction(
|
savedAddr == connectedDevice && connStatusEnum == ConnectionStatus.CONNECTED -> SharedViewModel.TopBarAction(
|
||||||
icon = Icons.Filled.BluetoothConnected, // Icon for "connected"
|
icon = Icons.Filled.BluetoothConnected,
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_disconnect_desc, deviceNameForMessage),
|
contentDescription = context.getString(R.string.bluetooth_action_disconnect_desc, deviceNameForMessage),
|
||||||
onClick = {
|
onClick = {
|
||||||
// Trigger the action first, then show the Snackbar
|
bluetoothViewModel.disconnectDevice()
|
||||||
bluetoothViewModel.disconnectDevice() // IMPORTANT: Trigger disconnection here!
|
|
||||||
sharedViewModel.showSnackbar(
|
sharedViewModel.showSnackbar(
|
||||||
message = context.getString(R.string.snackbar_bluetooth_disconnecting_from, deviceNameForMessage), // Adjusted message
|
message = context.getString(R.string.snackbar_bluetooth_disconnecting_from, deviceNameForMessage),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Case 4: Connection error, and an address is saved
|
// 4) Disconnected / Idle / None / Failed → guarded connect (request first, connect later via callbacks)
|
||||||
connStatusEnum == ConnectionStatus.FAILED && savedAddr != null -> SharedViewModel.TopBarAction(
|
savedAddr != null && (
|
||||||
icon = Icons.Filled.Error, // Error icon
|
connStatusEnum == ConnectionStatus.DISCONNECTED ||
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_retry_connection_desc, deviceNameForMessage),
|
connStatusEnum == ConnectionStatus.IDLE ||
|
||||||
onClick = {
|
connStatusEnum == ConnectionStatus.NONE ||
|
||||||
sharedViewModel.showSnackbar(
|
connStatusEnum == ConnectionStatus.FAILED
|
||||||
message = context.getString(R.string.snackbar_bluetooth_retry_connection, deviceNameForMessage),
|
) -> SharedViewModel.TopBarAction(
|
||||||
duration = SnackbarDuration.Short
|
icon = Icons.Filled.BluetoothDisabled,
|
||||||
)
|
|
||||||
bluetoothViewModel.connectToSavedDevice()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Case 5: Connection error, and NO address is saved
|
|
||||||
connStatusEnum == ConnectionStatus.FAILED && savedAddr == null -> SharedViewModel.TopBarAction(
|
|
||||||
icon = Icons.Filled.Error, // Error icon
|
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_error_check_settings_desc),
|
|
||||||
onClick = {
|
|
||||||
sharedViewModel.showSnackbar(
|
|
||||||
message = context.getString(R.string.snackbar_bluetooth_error_check_settings),
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
currentNavController.navigate(Routes.BLUETOOTH_SETTINGS)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Case 6: Saved device exists but is not connected (disconnected, idle, etc.)
|
|
||||||
// This case also covers if connStatusEnum = DISCONNECTED, IDLE, or NONE.
|
|
||||||
savedAddr != null && (connStatusEnum == ConnectionStatus.DISCONNECTED || connStatusEnum == ConnectionStatus.IDLE || connStatusEnum == ConnectionStatus.NONE) -> SharedViewModel.TopBarAction(
|
|
||||||
icon = Icons.Filled.BluetoothDisabled, // Icon for "disconnected" or "ready to connect"
|
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_connect_to_desc, deviceNameForMessage),
|
contentDescription = context.getString(R.string.bluetooth_action_connect_to_desc, deviceNameForMessage),
|
||||||
onClick = {
|
onClick = onClick@{
|
||||||
|
// a) Ask for permissions if missing (do NOT connect here; wait for onResult)
|
||||||
|
if (!bluetoothViewModel.permissionsGranted.value) {
|
||||||
|
requestBtPermissions()
|
||||||
|
return@onClick
|
||||||
|
}
|
||||||
|
// b) Ask to enable BT if off (do NOT connect here; wait for onActivityResult)
|
||||||
|
if (!bluetoothViewModel.isBluetoothEnabled()) {
|
||||||
|
requestEnableBluetooth()
|
||||||
|
return@onClick
|
||||||
|
}
|
||||||
|
// c) All good → connect now (explicit user tap; no autoconn)
|
||||||
sharedViewModel.showSnackbar(
|
sharedViewModel.showSnackbar(
|
||||||
message = context.getString(R.string.snackbar_bluetooth_attempting_connection, deviceNameForMessage),
|
message = context.getString(R.string.snackbar_bluetooth_attempting_connection, deviceNameForMessage),
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
@@ -237,43 +249,18 @@ fun determineBluetoothTopBarAction(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fallback: If an address is saved, but the state was not specifically covered above,
|
// 5) Fallback
|
||||||
// offer to connect. Ideally, this shouldn't be hit often if the logic above is complete.
|
else -> SharedViewModel.TopBarAction(
|
||||||
// If no device is saved and there's no error/connection attempt,
|
icon = Icons.Default.Bluetooth,
|
||||||
// this was already covered by 'savedAddr == null' (leads to settings).
|
contentDescription = context.getString(R.string.bluetooth_action_check_settings_desc),
|
||||||
else -> {
|
onClick = {
|
||||||
if (savedAddr != null) {
|
sharedViewModel.showSnackbar(
|
||||||
// This serves as a generic "Connect" button if a rare state occurs
|
message = context.getString(R.string.snackbar_bluetooth_check_settings),
|
||||||
SharedViewModel.TopBarAction(
|
duration = SnackbarDuration.Short
|
||||||
icon = Icons.Filled.BluetoothDisabled,
|
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_connect_to_desc, deviceNameForMessage),
|
|
||||||
onClick = {
|
|
||||||
sharedViewModel.showSnackbar(
|
|
||||||
message = context.getString(R.string.snackbar_bluetooth_attempting_connection, deviceNameForMessage),
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
bluetoothViewModel.connectToSavedDevice()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// If really no other condition applies and no device is saved,
|
|
||||||
// and the above cases haven't been met, "Go to settings" is a safe default.
|
|
||||||
// This will likely only be hit if connStatusEnum has an unexpected value
|
|
||||||
// and savedAddr is null, but that should already be covered by "Case 2".
|
|
||||||
// For safety, nonetheless:
|
|
||||||
SharedViewModel.TopBarAction(
|
|
||||||
icon = Icons.Default.Bluetooth,
|
|
||||||
contentDescription = context.getString(R.string.bluetooth_action_check_settings_desc),
|
|
||||||
onClick = {
|
|
||||||
sharedViewModel.showSnackbar(
|
|
||||||
message = context.getString(R.string.snackbar_bluetooth_check_settings),
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
currentNavController.navigate(Routes.BLUETOOTH_SETTINGS)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
currentNavController.navigate(Routes.BLUETOOTH_SETTINGS)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +317,51 @@ fun OverviewScreen(
|
|||||||
val connectedDeviceAddr by bluetoothViewModel.connectedDeviceAddress.collectAsState()
|
val connectedDeviceAddr by bluetoothViewModel.connectedDeviceAddress.collectAsState()
|
||||||
val savedDeviceNameString by bluetoothViewModel.savedScaleName.collectAsState()
|
val savedDeviceNameString by bluetoothViewModel.savedScaleName.collectAsState()
|
||||||
|
|
||||||
|
val enableBluetoothLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
bluetoothViewModel.refreshPermissionsStatus()
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
if (bluetoothViewModel.permissionsGranted.value) {
|
||||||
|
// Permissions and BT are fine → safe to connect now
|
||||||
|
bluetoothViewModel.connectToSavedDevice()
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
sharedViewModel.showSnackbar(
|
||||||
|
message = context.getString(R.string.bluetooth_enabled_permissions_missing),
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scope.launch {
|
||||||
|
sharedViewModel.showSnackbar(
|
||||||
|
message = context.getString(R.string.bt_snackbar_bluetooth_disabled_to_connect_default),
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val permissionsLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { result ->
|
||||||
|
bluetoothViewModel.refreshPermissionsStatus()
|
||||||
|
val allGranted = result.values.all { it }
|
||||||
|
if (allGranted) {
|
||||||
|
if (bluetoothViewModel.isBluetoothEnabled()) {
|
||||||
|
bluetoothViewModel.connectToSavedDevice()
|
||||||
|
} else {
|
||||||
|
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
enableBluetoothLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sharedViewModel.showSnackbar(R.string.bt_snackbar_permissions_required_to_connect_default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Determine the Bluetooth action for the top bar
|
// Determine the Bluetooth action for the top bar
|
||||||
val bluetoothTopBarAction = determineBluetoothTopBarAction(
|
val bluetoothTopBarAction = determineBluetoothTopBarAction(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -339,7 +371,9 @@ fun OverviewScreen(
|
|||||||
currentNavController = navController,
|
currentNavController = navController,
|
||||||
bluetoothViewModel = bluetoothViewModel,
|
bluetoothViewModel = bluetoothViewModel,
|
||||||
sharedViewModel = sharedViewModel,
|
sharedViewModel = sharedViewModel,
|
||||||
currentDeviceName = savedDeviceNameString
|
currentDeviceName = savedDeviceNameString,
|
||||||
|
permissionsLauncher = permissionsLauncher,
|
||||||
|
enableBluetoothLauncher = enableBluetoothLauncher
|
||||||
)
|
)
|
||||||
|
|
||||||
// LaunchedEffect to configure the top bar based on the current state
|
// LaunchedEffect to configure the top bar based on the current state
|
||||||
|
@@ -56,9 +56,13 @@ import androidx.compose.material3.OutlinedCard
|
|||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -94,31 +98,15 @@ fun BluetoothScreen(
|
|||||||
val scanError by bluetoothViewModel.scanError.collectAsState()
|
val scanError by bluetoothViewModel.scanError.collectAsState()
|
||||||
val connectionError by bluetoothViewModel.connectionError.collectAsState()
|
val connectionError by bluetoothViewModel.connectionError.collectAsState()
|
||||||
val hasPermissions by bluetoothViewModel.permissionsGranted.collectAsState()
|
val hasPermissions by bluetoothViewModel.permissionsGranted.collectAsState()
|
||||||
|
var pendingScan by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val savedDeviceAddress by bluetoothViewModel.savedScaleAddress.collectAsState()
|
val savedDeviceAddress by bluetoothViewModel.savedScaleAddress.collectAsState()
|
||||||
val savedDeviceName by bluetoothViewModel.savedScaleName.collectAsState()
|
val savedDeviceName by bluetoothViewModel.savedScaleName.collectAsState()
|
||||||
|
|
||||||
val permissionsLauncher = rememberLauncherForActivityResult(
|
DisposableEffect(Unit) {
|
||||||
contract = ActivityResultContracts.RequestMultiplePermissions()
|
onDispose {
|
||||||
) { permissionsMap ->
|
pendingScan = false
|
||||||
bluetoothViewModel.refreshPermissionsStatus()
|
bluetoothViewModel.requestStopDeviceScan()
|
||||||
val allGranted = permissionsMap.values.all { it }
|
|
||||||
if (allGranted) {
|
|
||||||
if (!bluetoothViewModel.isBluetoothEnabled()) {
|
|
||||||
scope.launch {
|
|
||||||
sharedViewModel.showSnackbar(
|
|
||||||
message = context.getString(R.string.bluetooth_enable_for_scan),
|
|
||||||
duration = SnackbarDuration.Short
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scope.launch {
|
|
||||||
sharedViewModel.showSnackbar(
|
|
||||||
message = context.getString(R.string.bluetooth_permissions_required_for_scan),
|
|
||||||
duration = SnackbarDuration.Long
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +115,19 @@ fun BluetoothScreen(
|
|||||||
) { result ->
|
) { result ->
|
||||||
bluetoothViewModel.refreshPermissionsStatus()
|
bluetoothViewModel.refreshPermissionsStatus()
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
if (!bluetoothViewModel.permissionsGranted.value) {
|
if (bluetoothViewModel.permissionsGranted.value) {
|
||||||
|
if (pendingScan) {
|
||||||
|
bluetoothViewModel.clearAllErrors()
|
||||||
|
if (!bluetoothViewModel.isScanning.value) {
|
||||||
|
bluetoothViewModel.requestStartDeviceScan()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
pendingScan = false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
sharedViewModel.showSnackbar(
|
sharedViewModel.showSnackbar(
|
||||||
message = context.getString(R.string.bluetooth_enabled_permissions_missing),
|
message = context.getString(R.string.bluetooth_enabled_permissions_missing),
|
||||||
@@ -145,6 +145,36 @@ fun BluetoothScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val permissionsLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestMultiplePermissions()
|
||||||
|
) { permissionsMap ->
|
||||||
|
bluetoothViewModel.refreshPermissionsStatus()
|
||||||
|
val allGranted = permissionsMap.values.all { it }
|
||||||
|
if (allGranted) {
|
||||||
|
if (bluetoothViewModel.isBluetoothEnabled()) {
|
||||||
|
if (pendingScan) {
|
||||||
|
bluetoothViewModel.clearAllErrors()
|
||||||
|
if (!bluetoothViewModel.isScanning.value) {
|
||||||
|
bluetoothViewModel.requestStartDeviceScan()
|
||||||
|
}
|
||||||
|
pendingScan = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
enableBluetoothLauncher.launch(enableBtIntent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pendingScan = false
|
||||||
|
scope.launch {
|
||||||
|
sharedViewModel.showSnackbar(
|
||||||
|
message = context.getString(R.string.bluetooth_permissions_required_for_scan),
|
||||||
|
duration = SnackbarDuration.Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -156,6 +186,7 @@ fun BluetoothScreen(
|
|||||||
// Status and action area (Scan button or info cards)
|
// Status and action area (Scan button or info cards)
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
PermissionRequestCard(onGrantPermissions = {
|
PermissionRequestCard(onGrantPermissions = {
|
||||||
|
pendingScan = true
|
||||||
permissionsLauncher.launch(
|
permissionsLauncher.launch(
|
||||||
arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
|
||||||
)
|
)
|
||||||
@@ -203,8 +234,26 @@ fun BluetoothScreen(
|
|||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
bluetoothViewModel.requestStopDeviceScan()
|
bluetoothViewModel.requestStopDeviceScan()
|
||||||
} else {
|
} else {
|
||||||
bluetoothViewModel.clearAllErrors() // Clear previous errors before starting a new scan
|
pendingScan = true
|
||||||
bluetoothViewModel.requestStartDeviceScan()
|
bluetoothViewModel.clearAllErrors()
|
||||||
|
when {
|
||||||
|
!bluetoothViewModel.permissionsGranted.value -> {
|
||||||
|
permissionsLauncher.launch(
|
||||||
|
arrayOf(
|
||||||
|
Manifest.permission.BLUETOOTH_SCAN,
|
||||||
|
Manifest.permission.BLUETOOTH_CONNECT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
!bluetoothViewModel.isBluetoothEnabled() -> {
|
||||||
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
enableBluetoothLauncher.launch(enableBtIntent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
bluetoothViewModel.requestStartDeviceScan()
|
||||||
|
pendingScan = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -440,7 +489,7 @@ fun DeviceCardItem(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val supportColor = if (deviceInfo.isSupported) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
val supportColor = if (deviceInfo.isSupported) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
val unknownDeviceName = stringResource(R.string.unknown_device_placeholder)
|
val unknownDeviceName = stringResource(R.string.unknown_device)
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
@@ -190,7 +190,6 @@
|
|||||||
<string name="bluetooth_must_be_enabled_for_scan">Bluetooth muss aktiviert sein, um nach Waagen zu suchen.</string>
|
<string name="bluetooth_must_be_enabled_for_scan">Bluetooth muss aktiviert sein, um nach Waagen zu suchen.</string>
|
||||||
<string name="saved_scale_label">Gespeicherte Waage:</string>
|
<string name="saved_scale_label">Gespeicherte Waage:</string>
|
||||||
<string name="unknown_device">Unbekannt</string>
|
<string name="unknown_device">Unbekannt</string>
|
||||||
<string name="unknown_device_placeholder">Unbekanntes Gerät</string>
|
|
||||||
<string name="stop_scan_button">Scan stoppen</string>
|
<string name="stop_scan_button">Scan stoppen</string>
|
||||||
<string name="search_for_scales_button">Nach Waagen suchen</string>
|
<string name="search_for_scales_button">Nach Waagen suchen</string>
|
||||||
<string name="search_for_scales_button_desc">Schaltfläche "Nach Waagen suchen"</string>
|
<string name="search_for_scales_button_desc">Schaltfläche "Nach Waagen suchen"</string>
|
||||||
|
@@ -192,7 +192,6 @@
|
|||||||
<string name="bluetooth_must_be_enabled_for_scan">Bluetooth must be enabled to search for scales.</string>
|
<string name="bluetooth_must_be_enabled_for_scan">Bluetooth must be enabled to search for scales.</string>
|
||||||
<string name="saved_scale_label">Saved Scale:</string>
|
<string name="saved_scale_label">Saved Scale:</string>
|
||||||
<string name="unknown_device">Unknown</string>
|
<string name="unknown_device">Unknown</string>
|
||||||
<string name="unknown_device_placeholder">Unknown Device</string>
|
|
||||||
<string name="stop_scan_button">Stop Scan</string>
|
<string name="stop_scan_button">Stop Scan</string>
|
||||||
<string name="search_for_scales_button">Search for Scales</string>
|
<string name="search_for_scales_button">Search for Scales</string>
|
||||||
<string name="search_for_scales_button_desc">Search for scales button</string>
|
<string name="search_for_scales_button_desc">Search for scales button</string>
|
||||||
|
Reference in New Issue
Block a user