diff --git a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java index 97b61dda0..9669a2cea 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/io/file/importer/ExportImportTest.java @@ -586,7 +586,7 @@ public class ExportImportTest { Mockito.when(barometer.getValue()).thenReturn(new AltitudeGainLoss(altitudeGain, altitudeGain)); sensorDataSet.add(barometer); } else { - sensorDataSet.add(new AggregatorBarometer("")); + sensorDataSet.add(new AggregatorBarometer("test", null)); } } diff --git a/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java index 5ef2b51c6..88db170fc 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometerTest.java @@ -16,7 +16,7 @@ public class AggregatorBarometerTest { @Test public void getAltitudeGainLoss_downhill() { // given - AggregatorBarometer subject = new AggregatorBarometer(""); + AggregatorBarometer subject = new AggregatorBarometer("test", null); // then addSensorValue(subject, new float[]{1015f, 1015.01f, 1015.02f, 1015.03f, 1015.04f, 1015.05f, 1015.06f, 1015.07f, 1015.08f, 1015.09f, 1015.10f, 1015.11f, 1015.12f, 1015.13f, 1018f, 1018.1f, 1018.1f, 1018.1f, 1018.1f}); diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java index 2c0cf74f0..c3aabdc95 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothConnectionManager.java @@ -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 @@ -101,9 +105,12 @@ public class BluetoothConnectionManager { return; } - descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); - gatt.writeDescriptor(descriptor); - + if (!descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)) { + Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID could not be set to ENABLE_NOTIFICATION_VALUE"); + } + if (!gatt.writeDescriptor(descriptor)) { + Log.e(TAG, "CLIENT_CHARACTERISTIC_CONFIG_UUID descriptor could not be written"); + } } @Override @@ -124,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); @@ -146,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"); @@ -158,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; + } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java index ef7aeba37..299fa4a8b 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothHandlerBarometricPressure.java @@ -1,12 +1,15 @@ package de.dennisguse.opentracks.sensors; import android.bluetooth.BluetoothGattCharacteristic; +import android.util.Log; import java.util.List; import java.util.UUID; import de.dennisguse.opentracks.data.models.AtmosphericPressure; import de.dennisguse.opentracks.sensors.sensorData.Aggregator; +import de.dennisguse.opentracks.sensors.sensorData.AggregatorBarometer; +import de.dennisguse.opentracks.sensors.sensorData.Raw; import de.dennisguse.opentracks.sensors.sensorData.SensorHandlerInterface; public class BluetoothHandlerBarometricPressure implements SensorHandlerInterface { @@ -23,14 +26,22 @@ public class BluetoothHandlerBarometricPressure implements SensorHandlerInterfac @Override public Aggregator createEmptySensorData(String address, String name) { - return null; //TODO + return new AggregatorBarometer(address, name); } @Override public void handlePayload(SensorManager.SensorDataChangedObserver observer, ServiceMeasurementUUID serviceMeasurementUUID, String sensorName, String address, BluetoothGattCharacteristic characteristic) { - //TODO + 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(); diff --git a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java index 28f65f304..8f919fca2 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/BluetoothRemoteSensorManager.java @@ -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,29 +28,20 @@ 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. *

* Note: should only be instantiated once. - *

* TODO: listen for Bluetooth enabled/disabled events. - *

- * TODO: In case, a cycling (Cadence and Speed) sensor reports both values, testing is required. - * We establish two GATT separate GATT connections (as if two different sensors were used). - * However, it is not clear if this is allowed. - * Even if this works, it is not clear what happens if a user (while recording) changes one of the sensors in the settings as this will trigger a disconnect of one GATT. - * * @author Sandor Dornbush */ public class BluetoothRemoteSensorManager implements SensorConnector, SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = BluetoothRemoteSensorManager.class.getSimpleName(); - public static final Duration MAX_SENSOR_DATE_SET_AGE = Duration.ofSeconds(50); //TODO + 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; @@ -66,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 @@ -95,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 (PreferencesUtils.isBluetoothSensorAddressNone(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 diff --git a/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java b/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java index 40e1749f9..6dca516d4 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/GainManager.java @@ -1,13 +1,14 @@ package de.dennisguse.opentracks.sensors; import android.content.Context; +import android.content.SharedPreferences; import android.os.Handler; import android.util.Log; -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; /** * Estimates the altitude gain and altitude loss using the device's pressure sensor (i.e., barometer). @@ -16,31 +17,61 @@ public class GainManager implements SensorConnector { private static final String TAG = GainManager.class.getSimpleName(); - private final BarometerInternal driver; private final SensorManager.SensorDataChangedObserver listener; + private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> { + connect(); + }; + + 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) { - driver.connect(context, handler, this); - - if (driver.isConnected()) { - listener.onConnect(new AggregatorBarometer("internal")); - } + this.context = context; + this.handler = handler; + PreferencesUtils.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); } public void stop(Context context) { Log.d(TAG, "Stop"); + this.context = null; + this.handler = null; + PreferencesUtils.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - driver.disconnect(context); - listener.onDisconnect(new AggregatorBarometer("internal")); + onDisconnect(); } - public void onSensorValueChanged(AtmosphericPressure currentSensorValue) { - listener.onChange(new Raw<>(currentSensorValue)); + private void connect() { + onDisconnect(); + + String address = PreferencesUtils.getBarometerSensorAddress(); + switch (PreferencesUtils.getSensorType(address)) { + case NONE -> driver = null; + 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, address); + } + } + + private void onDisconnect() { + if (driver == null) return; + + driver.disconnect(); + listener.onDisconnect(new AggregatorBarometer("GainManager", null)); } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/SensorType.java b/src/main/java/de/dennisguse/opentracks/sensors/SensorType.java new file mode 100644 index 000000000..ad66d7fce --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/SensorType.java @@ -0,0 +1,17 @@ +package de.dennisguse.opentracks.sensors; + +public enum SensorType { + NONE("NONE"), + INTERNAL("INTERNAL"), + REMOTE("*"); + + private final String preferenceValue; + + SensorType(String preferenceValue) { + this.preferenceValue = preferenceValue; + } + + public String getPreferenceValue() { + return preferenceValue; + } +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java b/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java index 9032010a5..74ef061f8 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/driver/BarometerInternal.java @@ -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; } } diff --git a/src/main/java/de/dennisguse/opentracks/sensors/driver/Driver.java b/src/main/java/de/dennisguse/opentracks/sensors/driver/Driver.java new file mode 100644 index 000000000..98157de60 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/sensors/driver/Driver.java @@ -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(); +} diff --git a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java index 7e5ed7c57..06f9fbb7e 100644 --- a/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java +++ b/src/main/java/de/dennisguse/opentracks/sensors/sensorData/AggregatorBarometer.java @@ -11,8 +11,8 @@ public class AggregatorBarometer extends Aggregator) data); return; } diff --git a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java index 6f5c5affb..531aa4fbf 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java +++ b/src/main/java/de/dennisguse/opentracks/settings/PreferencesUtils.java @@ -48,6 +48,7 @@ import de.dennisguse.opentracks.data.models.HeartRateZones; import de.dennisguse.opentracks.data.models.Track; import de.dennisguse.opentracks.io.file.TrackFileFormat; import de.dennisguse.opentracks.io.file.TrackFilenameGenerator; +import de.dennisguse.opentracks.sensors.SensorType; import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils; import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayout; import de.dennisguse.opentracks.ui.customRecordingLayout.RecordingLayoutIO; @@ -249,11 +250,18 @@ public class PreferencesUtils { } private static String getBluetoothSensorAddressNone() { - return resources.getString(R.string.sensor_type_value_none); + 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)) + .findFirst() + .orElse(SensorType.REMOTE); + } + + public static String getBarometerSensorAddress() { + return getString(R.string.settings_sensor_bluetooth_pressure_key, getBluetoothSensorAddressNone()); } public static String getBluetoothHeartRateSensorAddress() { diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeAdapter.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeAdapter.java index c60a3f4a9..328f9b152 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeAdapter.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeAdapter.java @@ -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 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); } } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java index 025251bb0..4873687c0 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingPowerPreference.java @@ -5,6 +5,8 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; +import java.util.List; + import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower; public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreference { @@ -27,6 +29,6 @@ public class BluetoothLeCyclingPowerPreference extends BluetoothLeSensorPreferen @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerManagerCyclingPower.CYCLING_POWER); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerManagerCyclingPower.CYCLING_POWER)); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java index 5b37a9ca3..bddce5a21 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeCyclingSpeedPreference.java @@ -5,6 +5,8 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; +import java.util.List; + import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed; public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreference { @@ -27,6 +29,6 @@ public class BluetoothLeCyclingSpeedPreference extends BluetoothLeSensorPreferen @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerCyclingDistanceSpeed.CYCLING_SPEED_CADENCE)); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLePressurePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLePressurePreference.java new file mode 100644 index 000000000..ee3e495a9 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLePressurePreference.java @@ -0,0 +1,41 @@ +package de.dennisguse.opentracks.settings.bluetooth; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.util.AttributeSet; + +import androidx.preference.PreferenceDialogFragmentCompat; + +import java.util.List; + +import de.dennisguse.opentracks.sensors.BluetoothHandlerBarometricPressure; + +public class BluetoothLePressurePreference extends BluetoothLeSensorPreference { + + public BluetoothLePressurePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BluetoothLePressurePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BluetoothLePressurePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BluetoothLePressurePreference(Context context) { + super(context); + } + + @Override + public PreferenceDialogFragmentCompat createInstance() { + SensorManager sensorManager = (SensorManager) getContext().getSystemService(Context.SENSOR_SERVICE); + Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE); + boolean hasInternalSensor = pressureSensor != null; + + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog + .newInstance(getKey(), List.of(BluetoothHandlerBarometricPressure.BAROMETRIC_PRESSURE), hasInternalSensor); + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java index d857ace9e..470b8d636 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeRunningSpeedAndCadencePreference.java @@ -5,6 +5,8 @@ import android.util.AttributeSet; import androidx.preference.PreferenceDialogFragmentCompat; +import java.util.List; + import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence; public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSensorPreference { @@ -27,6 +29,6 @@ public class BluetoothLeRunningSpeedAndCadencePreference extends BluetoothLeSens @Override public PreferenceDialogFragmentCompat createInstance() { - return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE); + return BluetoothLeSensorPreference.BluetoothLeSensorPreferenceDialog.newInstance(getKey(), List.of(BluetoothHandlerRunningSpeedAndCadence.RUNNING_SPEED_CADENCE)); } } diff --git a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java index e84f55849..013c2be98 100644 --- a/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java +++ b/src/main/java/de/dennisguse/opentracks/settings/bluetooth/BluetoothLeSensorPreference.java @@ -23,12 +23,12 @@ import androidx.preference.PreferenceDialogFragmentCompat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.sensors.BluetoothUtils; +import de.dennisguse.opentracks.sensors.SensorType; import de.dennisguse.opentracks.sensors.ServiceMeasurementUUID; import de.dennisguse.opentracks.settings.PreferencesUtils; import de.dennisguse.opentracks.util.PermissionRequester; @@ -43,9 +43,12 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { private static final String TAG = BluetoothLeSensorPreference.class.getSimpleName(); private static final String ARG_BLE_SERVICE_UUIDS = "bluetoothUUID"; + private static final String ARG_INCLUDE_INTERNAL = "supportsInternal"; private static final int DEVICE_NONE_RESOURCEID = R.string.value_none; + private static final int SENSOR_INTERNAL_RESOURCEID = R.string.value_internal_sensor; + public BluetoothLeSensorPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -65,11 +68,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { private String value; private boolean valueSet = false; - public String getValue() { - return value; - } - - public void setValue(String value) { + private void setValue(String value) { final boolean changed = !TextUtils.equals(this.value, value); if (changed || !valueSet) { this.value = value; @@ -88,21 +87,24 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { @Override public CharSequence getSummary() { - if (getValue() == null || PreferencesUtils.isBluetoothSensorAddressNone(getValue())) { + if (value == null || SensorType.NONE.getPreferenceValue().equals(value)) { return getContext().getString(DEVICE_NONE_RESOURCEID); } + if (SensorType.INTERNAL.getPreferenceValue().equals(value)) { + return getContext().getString(SENSOR_INTERNAL_RESOURCEID); + } BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext()); if (bluetoothAdapter == null) { Log.w(TAG, "No Bluetooth adapter present"); - return getValue(); + return value; } - BluetoothDevice device = bluetoothAdapter.getRemoteDevice(getValue()); + 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 getValue(); + return value; } public abstract PreferenceDialogFragmentCompat createInstance(); @@ -119,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 results) { - for (ScanResult result : results) { - onScanResult(-1, result); - } + listAdapter.addAll(results.stream().map(ScanResult::getDevice).collect(Collectors.toList())); } @Override @@ -138,18 +138,20 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { } }; - public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, ServiceMeasurementUUID sensorUUID) { - return newInstance(preferenceKey, Collections.singletonList(sensorUUID)); + public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List sensorUUIDs) { + return newInstance(preferenceKey, sensorUUIDs, false); } - public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List sensorUUIDs) { + public static BluetoothLeSensorPreferenceDialog newInstance(String preferenceKey, List sensorUUIDs, boolean includeInternalSensor) { final BluetoothLeSensorPreferenceDialog fragment = new BluetoothLeSensorPreferenceDialog(); - final Bundle b = new Bundle(1); + final Bundle b = new Bundle(3); b.putString(ARG_KEY, preferenceKey); b.putParcelableArrayList(ARG_BLE_SERVICE_UUIDS, new ArrayList<>(sensorUUIDs.stream() .map(ServiceMeasurementUUID::serviceUUID) .map(ParcelUuid::new) .collect(Collectors.toList()))); + b.putBoolean(ARG_INCLUDE_INTERNAL, includeInternalSensor); + fragment.setArguments(b); return fragment; } @@ -179,6 +181,7 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { private void startBluetoothScan() { List serviceUUIDs = getArguments().getParcelableArrayList(ARG_BLE_SERVICE_UUIDS); + boolean includeInternalSensor = getArguments().getBoolean(ARG_INCLUDE_INTERNAL); BluetoothAdapter bluetoothAdapter = BluetoothUtils.getAdapter(getContext()); if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { @@ -200,15 +203,21 @@ public abstract class BluetoothLeSensorPreference extends DialogPreference { return; } - String deviceNone = getContext().getString(R.string.sensor_type_value_none); - 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.getValue(); - if (deviceSelected != null && !deviceNone.equals(deviceSelected)) { - listAdapter.add(preference.getValue(), preference.getValue()); - selectedEntryIndex = 1; + String deviceSelected = preference.value; + if (includeInternalSensor) { + listAdapter.add(SensorType.INTERNAL.getPreferenceValue(), getString(SENSOR_INTERNAL_RESOURCEID)); + if (SensorType.INTERNAL.getPreferenceValue().equals(deviceSelected)) { + selectedEntryIndex = 1; + } + } + + if (deviceSelected != null && SensorType.REMOTE.equals(PreferencesUtils.getSensorType(deviceSelected))) { + listAdapter.add(preference.value, preference.value); + selectedEntryIndex = !includeInternalSensor ? 1 : 2; } List scanFilter = null; diff --git a/src/main/res/values/settings.xml b/src/main/res/values/settings.xml index f66ad622a..97e18a979 100644 --- a/src/main/res/values/settings.xml +++ b/src/main/res/values/settings.xml @@ -32,7 +32,9 @@ bluetoothCyclingSpeedSensor bluetoothCyclingPowerSensor bluetoothRunningSpeedAndCadenceSensor + bluetoothPressureSensor NONE + INTERNAL bluetoothCyclingSpeedWheelCircumference 2135 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 02bdf8955..44ffd5fc6 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -331,6 +331,7 @@ limitations under the License. bpm %1$d bpm Power + Barometer Avg Power Max Power W @@ -559,6 +560,7 @@ limitations under the License. %1$d s None Off + Internal sensor Smallest (recommended) %1$d s (recommended) Becoming idle. diff --git a/src/main/res/xml/settings_sensors.xml b/src/main/res/xml/settings_sensors.xml index 564a3e3f3..f72adcdaa 100644 --- a/src/main/res/xml/settings_sensors.xml +++ b/src/main/res/xml/settings_sensors.xml @@ -15,6 +15,10 @@ android:key="@string/settings_sensor_heart_rate_max_key" android:title="@string/settings_sensor_heart_rate_max" app:useSimpleSummaryProvider="true" /> +