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:
Dennis Guse
2019-11-12 18:30:47 +01:00
parent c59d1d87bd
commit e7a8c897a3
8 changed files with 358 additions and 79 deletions
@@ -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) {