Implement gain/loss via Bluetooth LE Environmental Sensing Sensor.

Fixes #1424.
This commit is contained in:
Dennis Guse
2023-12-28 15:10:44 +01:00
parent 8eed8e9d8d
commit 03dd0c2308
11 changed files with 170 additions and 134 deletions
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.sensors;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
@@ -33,19 +34,22 @@ import androidx.annotation.NonNull;
import java.util.Optional;
import java.util.UUID;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface;
/**
* Manages connection to a Bluetooth LE sensor and subscribes for onChange-notifications.
*/
@SuppressLint("MissingPermission")
public class BluetoothConnectionManager {
public class BluetoothConnectionManager implements Driver {
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
private final SensorManager.SensorDataChangedObserver observer;
private final SensorHandlerInterface sensorHandler;
private final BluetoothAdapter bluetoothAdapter;
private BluetoothGatt bluetoothGatt;
private final BluetoothGattCallback connectCallback = new BluetoothGattCallback() {
@Override
@@ -127,17 +131,46 @@ public class BluetoothConnectionManager {
}
};
BluetoothConnectionManager(SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
BluetoothConnectionManager(BluetoothAdapter bluetoothAdapter, SensorManager.SensorDataChangedObserver observer, SensorHandlerInterface sensorHandler) {
this.bluetoothAdapter = bluetoothAdapter;
this.observer = observer;
this.sensorHandler = sensorHandler;
}
synchronized void connect(Context context, Handler handler, @NonNull BluetoothDevice device) {
if (bluetoothGatt != null) {
@Override
public synchronized void connect(Context context, Handler handler, @NonNull String address) {
if (!isBluetoothEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (SensorType.NONE.getPreferenceValue().equals(address)) {
Log.w(TAG, "NONE: going to disconnect");
if (isConnected()) {
disconnect();
}
return;
}
if (isConnected()) {
Log.w(TAG, "Already connected; ignoring.");
return;
}
if (isSameBluetoothDevice(address)) {
return;
} else {
disconnect();
}
BluetoothDevice device;
try {
device = bluetoothAdapter.getRemoteDevice(address);
} catch (IllegalArgumentException e) {
Log.e(TAG, address + ": Unable to get remote device for", e);
return;
}
Log.d(TAG, device + ": trying to connect");
bluetoothGatt = device.connectGatt(context, false, connectCallback, BluetoothDevice.TRANSPORT_AUTO, 0, handler);
@@ -149,8 +182,10 @@ public class BluetoothConnectionManager {
observer.onDisconnect(sensorHandler.createEmptySensorData(bluetoothGatt.getDevice().getAddress(), bluetoothGatt.getDevice().getName()));
}
synchronized void disconnect() {
if (bluetoothGatt == null) {
@Override
public synchronized void disconnect() {
if (!isConnected()) {
Log.w(TAG, "Not connected; no need to re-connect.");
return;
}
Log.i(TAG, bluetoothGatt.getDevice() + ": start disconnect");
@@ -161,11 +196,21 @@ public class BluetoothConnectionManager {
bluetoothGatt = null;
}
synchronized boolean isSameBluetoothDevice(String address) {
private synchronized boolean isSameBluetoothDevice(String address) {
if (bluetoothGatt == null) {
return false;
}
return address.equals(bluetoothGatt.getDevice().getAddress());
}
private boolean isBluetoothEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
@Override
public boolean isConnected() {
return bluetoothGatt != null;
}
}
@@ -1,6 +1,7 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.util.List;
import java.util.UUID;
@@ -32,9 +33,15 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac
public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) {
AtmosphericPressure value = parseEnvironmentalSensing(characteristic);
if (value == null) return;
Log.i("ZXCV", "VALUE: " + value);
observer.onChange(new Raw<>(value));
}
/**
* Decoding:
* org.bluetooth.service.environmental_sensing.xml
* org.bluetooth.characteristic.pressure.xml
*/
public static AtmosphericPressure parseEnvironmentalSensing(BluetoothGattCharacteristic characteristic) {
byte[] raw = characteristic.getValue();
@@ -17,11 +17,9 @@
package de.dennisguse.opentracks.sensors;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -30,7 +28,6 @@ import java.time.Duration;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.settings.PreferencesUtils;
import de.dennisguse.opentracks.util.PermissionRequester;
/**
* Bluetooth LE sensor manager: manages connections to Bluetooth LE sensors.
@@ -45,7 +42,6 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(5);
private final BluetoothAdapter bluetoothAdapter;
private final Context context;
private final Handler handler;
private boolean started = false;
@@ -59,14 +55,13 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
public BluetoothRemoteSensorManager(@NonNull Context context, @NonNull Handler handler, @Nullable SensorManager.SensorDataChangedObserver observer) {
this.context = context;
this.handler = handler;
bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(observer, new BluetoothHandlerRunningSpeedAndCadence());
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(context);
this.heartRate = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerHeartRate());
this.cyclingCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingCadence());
this.cyclingSpeed = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerCyclingDistanceSpeed());
this.cyclingPower = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerManagerCyclingPower());
this.runningSpeedAndCadence = new BluetoothConnectionManager(bluetoothAdapter, observer, new BluetoothHandlerRunningSpeedAndCadence());
}
@Override
@@ -88,39 +83,8 @@ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPref
started = false;
}
public boolean isEnabled() {
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
}
private synchronized void connect(BluetoothConnectionManager connectionManager, String address) {
if (!isEnabled()) {
Log.w(TAG, "Bluetooth not enabled.");
return;
}
if (SensorType.NONE.getPreferenceValue().equals(address)) {
Log.w(TAG, "No Bluetooth address.");
connectionManager.disconnect();
return;
}
// Check if there is an ongoing connection; if yes, check if the address changed.
if (connectionManager.isSameBluetoothDevice(address)) {
return;
} else {
connectionManager.disconnect();
}
if (!PermissionRequester.BLUETOOTH.hasPermission(context)) {
Log.w(TAG, "BLUETOOTH_SCAN and/or BLUETOOTH_CONNECT not granted; not connecting.");
}
Log.i(TAG, "Connecting to bluetooth address: " + address);
try {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(address);
connectionManager.connect(context, handler, device);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to get remote device for: " + address, e);
}
connectionManager.connect(context, handler, address);
}
@Override
@@ -5,12 +5,9 @@ import android.content.SharedPreferences;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.driver.BarometerInternal;
import de.dennisguse.opentracks.sensors.driver.Driver;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.settings.PreferencesUtils;
/**
@@ -20,7 +17,6 @@ public class GainManager implements SensorConnector {
private static final String TAG = GainManager.class.getSimpleName();
private BarometerInternal driver;
private final SensorManager.SensorDataChangedObserver listener;
@@ -30,10 +26,10 @@ public class GainManager implements SensorConnector {
private Context context;
private Handler handler;
private Driver driver;
public GainManager(SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
driver = new BarometerInternal();
}
public void start(Context context, Handler handler) {
@@ -48,37 +44,34 @@ public class GainManager implements SensorConnector {
this.handler = null;
PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
onDisconnect(context);
}
public void onSensorValueChanged(AtmosphericPressure currentSensorValue) {
listener.onChange(new Raw<>(currentSensorValue));
onDisconnect();
}
private void connect() {
onDisconnect(context);
onDisconnect();
String address = PreferencesUtils.getBarometerSensorAddress();
switch (PreferencesUtils.getSensorType(address)) {
case NONE -> driver = null;
case INTERNAL -> driver = new BarometerInternal();
case REMOTE -> throw new RuntimeException("Not implemented"); //TODO #1424
case INTERNAL -> driver = new BarometerInternal(listener);
case REMOTE -> driver =
new BluetoothConnectionManager(
BluetoothUtils.getAdapter(context),
listener,
new BluetoothHandlerBarometricPressure()
);
default -> throw new RuntimeException("Not implemented");
}
if (driver != null) {
driver.connect(context, handler, this);
if (driver.isConnected()) {
listener.onConnect(new AggregatorBarometer("internal", null));
}
driver.connect(context, handler, address);
}
}
private void onDisconnect(@NonNull Context context) {
private void onDisconnect() {
if (driver == null) return;
driver.disconnect(context);
listener.onDisconnect(new AggregatorBarometer("internal", null));
driver.disconnect();
listener.onDisconnect(new AggregatorBarometer("GainManager", null));
}
}
@@ -4,24 +4,29 @@ import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.sensors.GainManager;
import de.dennisguse.opentracks.sensors.SensorManager;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
public class BarometerInternal {
public class BarometerInternal implements Driver {
private static final String TAG = BarometerInternal.class.getSimpleName();
private static final int SAMPLING_PERIOD = (int) TimeUnit.SECONDS.toMicros(5);
private GainManager observer;
private final SensorManager.SensorDataChangedObserver listener;
private final SensorEventListener listener = new SensorEventListener() {
private Context context;
private final SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
if (!isConnected()) {
@@ -29,7 +34,7 @@ public class BarometerInternal {
return;
}
observer.onSensorValueChanged(AtmosphericPressure.ofHPA(event.values[0]));
listener.onChange(new Raw<>(AtmosphericPressure.ofHPA(event.values[0])));
}
@Override
@@ -38,30 +43,40 @@ public class BarometerInternal {
}
};
public void connect(Context context, Handler handler, GainManager observer) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
public BarometerInternal(@NonNull SensorManager.SensorDataChangedObserver listener) {
this.listener = listener;
}
@Override
public void connect(Context context, Handler handler, String addressIgnored) {
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
if (pressureSensor == null) {
Log.w(TAG, "No pressure sensor available.");
this.observer = null;
return;
}
if (sensorManager.registerListener(listener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.observer = observer;
if (sensorManager.registerListener(sensorEventListener, pressureSensor, SAMPLING_PERIOD, handler)) {
this.context = context;
listener.onConnect(new AggregatorBarometer("internal", null));
return;
}
disconnect(context);
}
public void disconnect(Context context) {
SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(listener);
observer = null;
disconnect();
}
public boolean isConnected() {
return observer != null;
return context != null;
}
@Override
public void disconnect() {
if (!isConnected()) return;
android.hardware.SensorManager sensorManager = (android.hardware.SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
sensorManager.unregisterListener(sensorEventListener);
this.context = null;
}
}
@@ -0,0 +1,12 @@
package de.dennisguse.opentracks.sensors.driver;
import android.content.Context;
import android.os.Handler;
public interface Driver {
void connect(Context context, Handler handler, String address);
boolean isConnected();
void disconnect();
}
@@ -120,10 +120,6 @@ public final class SensorDataSet {
return;
}
if (value instanceof AtmosphericPressure) {
if (this.barometer == null) {
//TODO Quick fix. This shouldn't happen, but it does. May be a race condition?
return;
}
this.barometer.add((Raw<AtmosphericPressure>) data);
return;
}
@@ -253,10 +253,6 @@ public class PreferencesUtils {
return SensorType.NONE.getPreferenceValue();
}
public static boolean isBluetoothSensorAddressNone(String currentValue) {
return getBluetoothSensorAddressNone().equals(currentValue);
}
public static SensorType getSensorType(String address) {
return Arrays.stream(SensorType.values())
.filter(it -> it.getPreferenceValue().equals(address))
@@ -1,5 +1,6 @@
package de.dennisguse.opentracks.settings.bluetooth;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.view.LayoutInflater;
import android.view.View;
@@ -7,10 +8,13 @@ import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@SuppressLint("MissingPermission")
public class BluetoothLeAdapter extends BaseAdapter {
/**
@@ -50,22 +54,30 @@ public class BluetoothLeAdapter extends BaseAdapter {
return currentView;
}
public void add(String name, String address) {
Device device = new Device(name, address);
/**
* @return Data changed?
*/
public boolean add(String address, String name) {
Device device = new Device(address, name);
if (!devices.contains(device)) {
devices.add(new Device(name, address));
devices.add(device);
return true;
} else {
for (Device currentDevice : devices) {
if (currentDevice.getAddress().equals(address)) {
currentDevice.setName(name);
if (currentDevice.address.equals(address)) {
currentDevice.name = name;
return true;
}
}
}
notifyDataSetChanged();
return false;
}
public void add(BluetoothDevice bluetoothDevice) {
add(bluetoothDevice.getName(), bluetoothDevice.getAddress());
public void addAll(List<BluetoothDevice> bluetoothDevices) {
boolean dataSetChanged = bluetoothDevices.stream()
.anyMatch(bluetoothDevice -> add(bluetoothDevice.getAddress(), bluetoothDevice.getName()));
if (dataSetChanged) notifyDataSetChanged();
}
public Device get(int index) {
@@ -73,19 +85,14 @@ public class BluetoothLeAdapter extends BaseAdapter {
}
public static class Device {
private String name;
@NonNull
private final String address;
private String name;
public Device(String name, String address) {
this.name = name;
Device(@NonNull String address, String name) {
Objects.requireNonNull(address);
this.address = address;
}
public String getNameOrAddress() {
return name != null ? name : getAddress();
}
public void setName(String name) {
this.name = name;
}
@@ -93,16 +100,21 @@ public class BluetoothLeAdapter extends BaseAdapter {
return address;
}
public String getNameOrAddress() {
return name != null ? name : address;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Device device)) return false;
return address.equals(device.address);
}
@Override
public int hashCode() {
return Objects.hash(name, address);
return Objects.hash(address);
}
}
}
@@ -7,7 +7,9 @@ import android.util.AttributeSet;
import androidx.preference.PreferenceDialogFragmentCompat;
import java.util.Collections;
import java.util.List;
import de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure;
public class BluetoothLePressurePreference extends BluetoothLeSensorPreference {
@@ -34,7 +36,6 @@ public class BluetoothLePressurePreference extends BluetoothLeSensorPreference {
boolean hasInternalSensor = pressureSensor != null;
return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog
// .newInstance(getKey(), List.of(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE), hasInternalSensor); //TODO #1424
.newInstance(getKey(), Collections.emptyList(), hasInternalSensor);
.newInstance(getKey(), List.of(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE), hasInternalSensor);
}
}
@@ -102,7 +102,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(value);
if (device != null && device.getName() != null) {
return getContext().getString(R.string.bluetooth_sensor_summary, device.getAddress(), device.getName());
return getContext().getString(R.string.bluetooth_sensor_summary, device.getAddress(), device.getName());
}
return value;
}
@@ -121,14 +121,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
@Override
public void onScanResult(int callbackType, ScanResult result) {
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
listAdapter.add(result.getDevice());
onBatchScanResults(List.of(result));
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result : results) {
onScanResult(-1, result);
}
listAdapter.addAll(results.stream().map(ScanResult::getDevice).collect(Collectors.toList()));
}
@Override
@@ -205,24 +203,21 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference {
return;
}
String deviceNone = getContext().getString(R.string.sensor_type_value_none);
String sensorInternal = getString(R.string.sensor_type_value_internal);
listAdapter.add(getContext().getString(DEVICE_NONE_RESOURCEID), deviceNone);
listAdapter.add(SensorType.NONE.getPreferenceValue(), getContext().getString(DEVICE_NONE_RESOURCEID));
selectedEntryIndex = 0;
BluetoothLeSensorPreference preference = (BluetoothLeSensorPreference) getPreference();
String deviceSelected = preference.value;
if (includeInternalSensor) {
listAdapter.add(getString(SENSOR_INTERNAL_RESOURCEID), sensorInternal);
if (sensorInternal.equals(deviceSelected)) {
listAdapter.add(SensorType.INTERNAL.getPreferenceValue(), getString(SENSOR_INTERNAL_RESOURCEID));
if (SensorType.INTERNAL.getPreferenceValue().equals(deviceSelected)) {
selectedEntryIndex = 1;
}
}
if (deviceSelected != null && !deviceNone.equals(deviceSelected) && !sensorInternal.equals(deviceSelected)) {
if (deviceSelected != null && SensorType.REMOTE.equals(PreferencesUtils.getSensorType(deviceSelected))) {
listAdapter.add(preference.value, preference.value);
selectedEntryIndex++;
selectedEntryIndex = !includeInternalSensor ? 1 : 2;
}
List<ScanFilter> scanFilter = null;