forked from upstream-mirrors/OpenTracks
Settings: scan for Bluetooth sensors rather than using bonded devices.
Bonded devices do no provide information what services (e.g., heart rate) they support.
This commit is contained in:
+3
-2
@@ -45,7 +45,7 @@ public class BluetoothConnectionManager {
|
||||
static final int MESSAGE_READ = 3;
|
||||
static final int MESSAGE_DISCONNECTED = 4;
|
||||
|
||||
private static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
||||
public static final UUID HEART_RATE_SERVICE_UUID = new UUID(0x180D00001000L, 0x800000805f9b34fbL);
|
||||
private static final UUID HEART_RATE_MEASUREMENT_CHAR_UUID = new UUID(0x2A3700001000L, 0x800000805f9b34fbL);
|
||||
private static final UUID CLIENT_CHARACTERISTIC_CONFIG_UUID = new UUID(0x290200001000L, 0x800000805f9b34fbL);
|
||||
|
||||
@@ -131,8 +131,9 @@ public class BluetoothConnectionManager {
|
||||
|
||||
Log.d(TAG, "Connecting to: " + bluetoothDevice);
|
||||
|
||||
setState(SensorState.CONNECTING);
|
||||
bluetoothGatt = bluetoothDevice.connectGatt(this.context, true, this.connectCallback);
|
||||
|
||||
setState(SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BluetoothLeAdapter extends BaseAdapter {
|
||||
|
||||
/**
|
||||
* Contains a unique list (by address) of devices.
|
||||
*/
|
||||
private final List<Device> devices = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return devices.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return devices.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return devices.get(position).hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View currentView = convertView;
|
||||
if (convertView == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
//TODO Check if there is a better way to achieve identical look and feel to ListPreference.
|
||||
//Use material design single choice; for old style use: android.R.layout.select_dialog_singlechoice
|
||||
currentView = inflater.inflate(androidx.appcompat.R.layout.select_dialog_singlechoice_material, null);
|
||||
}
|
||||
|
||||
Device device = devices.get(position);
|
||||
TextView textView = currentView.findViewById(android.R.id.text1);
|
||||
textView.setText(device.getName());
|
||||
|
||||
return currentView;
|
||||
}
|
||||
|
||||
public void add(String name, String address) {
|
||||
Device device = new Device(name, address);
|
||||
if (!devices.contains(device)) {
|
||||
devices.add(new Device(name, address));
|
||||
} else {
|
||||
for (Device currentDevice : devices) {
|
||||
if (currentDevice.getAddress().equals(address)) {
|
||||
currentDevice.setName(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void add(BluetoothDevice bluetoothDevice) {
|
||||
add(bluetoothDevice.getName(), bluetoothDevice.getAddress());
|
||||
}
|
||||
|
||||
public Device get(int index) {
|
||||
return devices.get(index);
|
||||
}
|
||||
|
||||
public class Device {
|
||||
private String name;
|
||||
private String address;
|
||||
|
||||
public Device(String name, String address) {
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the address is identical.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Device)) return false;
|
||||
Device device = (Device) o;
|
||||
return address.equals(device.address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, address);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.le.BluetoothLeScanner;
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import android.bluetooth.le.ScanSettings;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelUuid;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.DialogPreference;
|
||||
import androidx.preference.PreferenceDialogFragmentCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.services.sensors.BluetoothConnectionManager;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
|
||||
/**
|
||||
* Preference to select a discoverable Bluetooth LE device.
|
||||
* Based upon ListPreference.
|
||||
*/
|
||||
public class BluetoothLeListPreference extends DialogPreference {
|
||||
|
||||
private final static String TAG = BluetoothLeListPreference.class.getSimpleName();
|
||||
|
||||
public BluetoothLeListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public BluetoothLeListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public BluetoothLeListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothLeListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private String value;
|
||||
private boolean valueSet = false;
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
final boolean changed = !TextUtils.equals(this.value, value);
|
||||
if (changed || !valueSet) {
|
||||
this.value = value;
|
||||
valueSet = true;
|
||||
persistString(value);
|
||||
if (changed) {
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(Object defaultValue) {
|
||||
setValue(getPersistedString((String) defaultValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getSummary() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
public static class BluetoothLeListPreferenceDialog extends PreferenceDialogFragmentCompat {
|
||||
|
||||
private int selectedEntryIndex;
|
||||
private BluetoothLeAdapter listAdapter = new BluetoothLeAdapter();
|
||||
|
||||
private BluetoothLeScanner scanner = null;
|
||||
private ScanCallback scanCallback = new ScanCallback() {
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
super.onScanResult(callbackType, result);
|
||||
Log.d(TAG, "Found device " + result.getDevice().getName() + " " + result);
|
||||
|
||||
listAdapter.add(result.getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results) {
|
||||
for (ScanResult scanResult : results) {
|
||||
listAdapter.add(scanResult.getDevice());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanFailed(int errorCode) {
|
||||
super.onScanFailed(errorCode);
|
||||
Log.e(TAG, "Bluetooth scan failed with errorCode " + errorCode);
|
||||
Toast.makeText(getContext(), R.string.sensor_could_not_scan, Toast.LENGTH_LONG).show();
|
||||
dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
static BluetoothLeListPreferenceDialog newInstance(String key) {
|
||||
final BluetoothLeListPreferenceDialog fragment = new BluetoothLeListPreferenceDialog();
|
||||
final Bundle b = new Bundle(1);
|
||||
b.putString(ARG_KEY, key);
|
||||
fragment.setArguments(b);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothUtils.getDefaultBluetoothAdapter(TAG);
|
||||
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth adapter is present or not enabled.");
|
||||
Toast.makeText(getContext(), R.string.bluetooth_disabled, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bluetoothAdapter.isDiscovering()) {
|
||||
Log.i(TAG, "Cancelling ongoing bluetooth discovery.");
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
scanner = bluetoothAdapter.getBluetoothLeScanner();
|
||||
if (scanner == null) {
|
||||
Log.e(TAG, "BluetoothLeScanner is null.");
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceNone = getContext().getString(R.string.sensor_type_value_none);
|
||||
listAdapter.add(getContext().getString(R.string.value_none), deviceNone);
|
||||
selectedEntryIndex = 0;
|
||||
|
||||
BluetoothLeListPreference preference = (BluetoothLeListPreference) getPreference();
|
||||
String deviceSelected = preference.getValue();
|
||||
if (!deviceNone.equals(deviceSelected)) {
|
||||
listAdapter.add(preference.getValue(), preference.getValue());
|
||||
selectedEntryIndex = 1;
|
||||
}
|
||||
|
||||
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothConnectionManager.HEART_RATE_SERVICE_UUID));
|
||||
List<ScanFilter> scanFilter = new ArrayList<>();
|
||||
scanFilter.add(scanFilterBuilder.build());
|
||||
|
||||
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
|
||||
scanSettingsBuilder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
|
||||
|
||||
scanner.startScan(scanFilter, scanSettingsBuilder.build(), scanCallback);
|
||||
}
|
||||
|
||||
//Behave like ListPreferenceDialogFragmentCompat, but uses a custom listAdapter.
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
|
||||
builder.setSingleChoiceItems(listAdapter, selectedEntryIndex,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
selectedEntryIndex = which;
|
||||
|
||||
BluetoothLeListPreferenceDialog.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setIcon(android.R.drawable.stat_sys_data_bluetooth);
|
||||
builder.setPositiveButton(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialogClosed(boolean positiveResult) {
|
||||
if (scanner != null) {
|
||||
scanner.stopScan(scanCallback);
|
||||
}
|
||||
|
||||
if (positiveResult && selectedEntryIndex >= 0) {
|
||||
String value = listAdapter.get(selectedEntryIndex).getAddress();
|
||||
BluetoothLeListPreference preference = (BluetoothLeListPreference) getPreference();
|
||||
if (preference.callChangeListener(value)) {
|
||||
preference.setValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,8 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.UnitConversions;
|
||||
|
||||
@@ -167,34 +160,4 @@ final class PreferenceHelper {
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the bluetooth sensor.
|
||||
*/
|
||||
static void configureBluetoothSensorList(ListPreference preference) {
|
||||
Context context = preference.getContext();
|
||||
|
||||
String value = PreferencesUtils.getString(context, R.string.settings_sensor_bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
|
||||
List<String> devicesNameList = new ArrayList<>();
|
||||
List<String> devicesAddressList = new ArrayList<>();
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bluetoothAdapter != null) {
|
||||
BluetoothUtils.populateDeviceLists(bluetoothAdapter, devicesNameList, devicesAddressList);
|
||||
}
|
||||
|
||||
// Was the previously configured device unpaired? Then forget it.
|
||||
if (!devicesAddressList.contains(value)) {
|
||||
value = PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT;
|
||||
PreferencesUtils.setString(context, R.string.settings_sensor_bluetooth_sensor_key, value);
|
||||
}
|
||||
|
||||
devicesNameList.add(0, context.getString(R.string.value_none));
|
||||
devicesAddressList.add(0, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
|
||||
|
||||
String[] values = devicesAddressList.toArray(new String[0]);
|
||||
preference.setEntryValues(values);
|
||||
|
||||
String[] options = devicesNameList.toArray(new String[0]);
|
||||
preference.setEntries(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.fragments.ChooseActivityTypeDialogFragment;
|
||||
import de.dennisguse.opentracks.util.BluetoothUtils;
|
||||
import de.dennisguse.opentracks.util.HackUtils;
|
||||
import de.dennisguse.opentracks.util.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity implements ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller, ResetDialogPreference.ResetCallback {
|
||||
|
||||
private static final String TAG = SettingsActivity.class.getSimpleName();
|
||||
|
||||
private PrefsFragment prefsFragment;
|
||||
|
||||
@Override
|
||||
@@ -54,13 +57,14 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.recording_track_id_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.stats_units_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -69,6 +73,14 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
}
|
||||
});
|
||||
}
|
||||
if (PreferencesUtils.isKey(getActivity(), R.string.chart_show_speed_key, key)) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateSpeed();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,7 +105,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
PreferencesUtils.getSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
updateUI();
|
||||
updateReset();
|
||||
updateBluetooth();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,6 +123,8 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
} else if (preference instanceof ActivityTypePreference) {
|
||||
activityPreferenceDialog = ActivityTypePreference.ActivityPreferenceDialog.newInstance(preference.getKey());
|
||||
dialogFragment = activityPreferenceDialog;
|
||||
} else if (preference instanceof BluetoothLeListPreference) {
|
||||
dialogFragment = BluetoothLeListPreference.BluetoothLeListPreferenceDialog.newInstance(preference.getKey());
|
||||
}
|
||||
|
||||
if (dialogFragment != null) {
|
||||
@@ -127,17 +142,22 @@ public class SettingsActivity extends AppCompatActivity implements ChooseActivit
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
private void updateReset() {
|
||||
final boolean isRecording = PreferencesUtils.isRecording(getActivity());
|
||||
Preference resetPreference = findPreference(getString(R.string.settings_reset_key));
|
||||
resetPreference.setSummary(isRecording ? getString(R.string.settings_not_while_recording) : "");
|
||||
resetPreference.setEnabled(!isRecording);
|
||||
}
|
||||
|
||||
private void updateSpeed() {
|
||||
Preference speedCheckBoxPreference = findPreference(getString(R.string.chart_show_speed_key));
|
||||
speedCheckBoxPreference.setTitle(PreferencesUtils.isReportSpeed(getActivity()) ? R.string.stats_speed : R.string.stats_pace);
|
||||
}
|
||||
|
||||
ListPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_sensor_key));
|
||||
PreferenceHelper.configureBluetoothSensorList(bluetoothPreference);
|
||||
private void updateBluetooth() {
|
||||
// Disable Bluetooth preference if device does not have Bluetooth
|
||||
BluetoothLeListPreference bluetoothPreference = findPreference(getString(R.string.settings_sensor_bluetooth_sensor_key));
|
||||
bluetoothPreference.setVisible(BluetoothUtils.hasBluetooth(TAG));
|
||||
}
|
||||
|
||||
private void updateUnits() {
|
||||
|
||||
@@ -16,16 +16,12 @@
|
||||
package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothClass;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bluetooth devices.
|
||||
@@ -74,26 +70,8 @@ public class BluetoothUtils {
|
||||
return adapters.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the device names and the device addresses with all the suitable
|
||||
* bluetooth devices.
|
||||
*
|
||||
* @param bluetoothAdapter the bluetooth adapter
|
||||
* @param deviceNames list of device names
|
||||
* @param deviceAddresses list of device addresses
|
||||
*/
|
||||
public static void populateDeviceLists(BluetoothAdapter bluetoothAdapter, List<String> deviceNames, List<String> deviceAddresses) {
|
||||
// Ensure the bluetooth adapter is not in discovery mode.
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
|
||||
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
BluetoothClass bluetoothClass = device.getBluetoothClass();
|
||||
if (bluetoothClass != null && (bluetoothClass.getDeviceClass() & BluetoothClass.Device.HEALTH_PULSE_RATE) != 0) {
|
||||
deviceAddresses.add(device.getAddress());
|
||||
deviceNames.add(device.getName());
|
||||
}
|
||||
}
|
||||
public static boolean hasBluetooth(final String TAG) {
|
||||
return BluetoothUtils.getDefaultBluetoothAdapter(TAG) != null;
|
||||
}
|
||||
|
||||
public static int parseHeartRate(BluetoothGattCharacteristic characteristic) {
|
||||
|
||||
Reference in New Issue
Block a user