1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-26 10:35:50 +01:00

Core: added first iteration of BLE intent API

Core: added BLE GATT Client

Core: fixed string comparisons

Core: unified intent APIs

Core: fixed notification and publication bugs

Core: extracted BLE Intent API logic

Core: introduced finer BLE API permissions

Core: use device name when adding test device through DiscoveryActivity

Core: avoid reporting same device state multiple times

Core: read firmware version on GATT Client connect connect

Core: use onSendConfiguration instead of direct subscription

Core: I18N for GATT API settings

Core: I18N for GATT API settings

Core: only show BLE API settings for BLE devices

Core: refactored intent handler

Core: extracted ble API to own class

Core: fixed unitialized BLE Api

BLE Intent API: I18N

BLE Intent API: refactoring

BLE Intent API: added back legacy API

BLE Intent API: removed new DEVICE_CHANGED and CONNECT endpoints

BLE Intent API: removed redundant ble api setting
This commit is contained in:
Daniel Dakhno 2024-08-24 00:41:19 +02:00 committed by José Rebelo
parent 0745a374a5
commit aae1d40d54
13 changed files with 520 additions and 22 deletions

View File

@ -621,7 +621,7 @@ public class DebugActivity extends AbstractGBActivity {
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
createTestDevice(DebugActivity.this, selectedTestDeviceKey, selectedTestDeviceMAC);
createTestDevice(DebugActivity.this, selectedTestDeviceKey, selectedTestDeviceMAC, null);
}
})
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
@ -1037,16 +1037,19 @@ public class DebugActivity extends AbstractGBActivity {
spinner.setOnItemSelectedListener(new CustomOnDeviceSelectedListener());
}
public static void createTestDevice(Context context, long deviceKey, String deviceMac) {
public static void createTestDevice(Context context, long deviceKey, String deviceMac, String deviceName) {
if (deviceKey == SELECT_DEVICE) {
return;
}
DeviceType deviceType = DeviceType.values()[(int) deviceKey];
String deviceName = deviceType.name();
int deviceNameResource = deviceType.getDeviceCoordinator().getDeviceNameResource();
if(deviceNameResource != 0){
deviceName = context.getString(deviceNameResource);
}
if(deviceName == null) {
int deviceNameResource = deviceType.getDeviceCoordinator().getDeviceNameResource();
if(deviceNameResource == 0){
deviceName = deviceType.name();
}else {
deviceName = context.getString(deviceNameResource);
}
};
try (
DBHandler db = GBApplication.acquireDB()) {
DaoSession daoSession = db.getDaoSession();
@ -1221,6 +1224,9 @@ public class DebugActivity extends AbstractGBActivity {
TreeMap <String, Pair<Long, Integer>> sortedMap = new TreeMap<>(newMap);
newMap = new LinkedHashMap<>(1);
newMap.put(app.getString(R.string.widget_settings_select_device_title), new Pair(SELECT_DEVICE, R.drawable.ic_device_unknown));
newMap.put(app.getString(R.string.devicetype_scannable), new Pair((long) DeviceType.SCANNABLE.ordinal(), R.drawable.ic_device_scannable));
newMap.put(app.getString(R.string.devicetype_ble_gatt_client), new Pair((long) DeviceType.BLE_GATT_CLIENT.ordinal(), R.drawable.ic_device_scannable));
newMap.putAll(sortedMap);
return newMap;

View File

@ -526,4 +526,9 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval";
public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter";
public static final String PREFS_KEY_DEVICE_BLE_API_DEVICE_STATE = "prefs_device_ble_api_state";
public static final String PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE = "prefs_device_ble_api_characteristic_read_write";
public static final String PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY = "prefs_device_ble_api_characteristic_notify";
public static final String PREFS_KEY_DEVICE_BLE_API_PACKAGE = "prefs_device_ble_api_package";
}

View File

@ -827,6 +827,11 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE);
addPreferenceHandlerFor(PREFS_KEY_DEVICE_BLE_API_DEVICE_STATE);
addPreferenceHandlerFor(PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE);
addPreferenceHandlerFor(PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY);
addPreferenceHandlerFor(PREFS_KEY_DEVICE_BLE_API_PACKAGE);
addPreferenceHandlerFor("lock");
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
@ -1307,6 +1312,12 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
DeviceSpecificSettingsScreen.DEVELOPER,
R.xml.devicesettings_settings_third_party_apps
);
if(coordinator.getConnectionType().usesBluetoothLE()) {
deviceSpecificSettings.addRootScreen(
DeviceSpecificSettingsScreen.DEVELOPER,
R.xml.devicesettings_ble_api
);
}
}
final DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer = coordinator.getDeviceSpecificSettingsCustomizer(device);

View File

@ -748,7 +748,12 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
.setView(linearLayout)
.setPositiveButton(R.string.ok, (dialog, which) -> {
if (selectedUnsupportedDeviceKey != DebugActivity.SELECT_DEVICE) {
DebugActivity.createTestDevice(DiscoveryActivityV2.this, selectedUnsupportedDeviceKey, deviceCandidate.getMacAddress());
DebugActivity.createTestDevice(
DiscoveryActivityV2.this,
selectedUnsupportedDeviceKey,
deviceCandidate.getMacAddress(),
deviceCandidate.getName()
);
finish();
}
})

View File

@ -257,6 +257,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs1pro.XiaomiWatc
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watchs3.XiaomiWatchS3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.gatt_client.BleGattClientCoordinator;
/**
* For every supported device, a device type constant must exist.
@ -500,6 +501,7 @@ public enum DeviceType {
COLMI_R06(ColmiR06Coordinator.class),
SCANNABLE(ScannableDeviceCoordinator.class),
CYCLING_SENSOR(CyclingSensorCoordinator.class),
BLE_GATT_CLIENT(BleGattClientCoordinator.class),
TEST(TestDeviceCoordinator.class);
private DeviceCoordinator coordinator;

View File

@ -101,7 +101,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLEScanService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BleIntentApi;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.AutoConnectIntervalReceiver;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBAutoFetchReceiver;
import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter;
@ -289,16 +291,17 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
"com.spotify.music.playbackstatechanged"
};
private final String COMMAND_BLUETOOTH_CONNECT = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECT";
private final String ACTION_DEVICE_CONNECTED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECTED";
private final String ACTION_DEVICE_SCANNED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_SCANNED";
private final int NOTIFICATIONS_CACHE_MAX = 10; // maximum amount of notifications to cache per device while disconnected
private boolean allowBluetoothIntentApi = false;
private boolean reconnectViaScan = GBPrefs.RECONNECT_SCAN_DEFAULT;
private final String API_LEGACY_COMMAND_BLUETOOTH_CONNECT = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECT";
private final String API_LEGACY_ACTION_DEVICE_CONNECTED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECTED";
private final String API_LEGACY_ACTION_DEVICE_SCANNED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_SCANNED";
private void sendDeviceAPIBroadcast(String address, String action){
if(!allowBluetoothIntentApi){
GB.log("not sending API event due to settings", GB.INFO, null);
LOG.debug("not sending API event due to settings");
return;
}
Intent intent = new Intent(action);
@ -308,14 +311,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
private void sendDeviceConnectedBroadcast(String address){
sendDeviceAPIBroadcast(address, ACTION_DEVICE_CONNECTED);
sendDeviceAPIBroadcast(address, API_LEGACY_ACTION_DEVICE_CONNECTED);
}
BroadcastReceiver bluetoothCommandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()){
case COMMAND_BLUETOOTH_CONNECT:
case API_LEGACY_COMMAND_BLUETOOTH_CONNECT:
if(!allowBluetoothIntentApi){
GB.log("Connection API not allowed in settings", GB.ERROR, null);
return;
@ -333,6 +336,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if(isDeviceConnected(address)){
GB.log(String.format("device %s already connected", address), GB.INFO, null);
sendDeviceConnectedBroadcast(address);
return;
}
@ -404,11 +408,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
GBDevice.DeviceUpdateSubject subject = (GBDevice.DeviceUpdateSubject) intent.getSerializableExtra(GBDevice.EXTRA_UPDATE_SUBJECT);
if(subject == GBDevice.DeviceUpdateSubject.DEVICE_STATE && device.isInitialized()){
LOG.debug("device state update reason");
sendDeviceConnectedBroadcast(device.getAddress());
sendCachedNotifications(device);
}else if(subject == GBDevice.DeviceUpdateSubject.CONNECTION_STATE && (device.getState() == GBDevice.State.SCANNED)){
sendDeviceAPIBroadcast(device.getAddress(), ACTION_DEVICE_SCANNED);
}else if(subject == GBDevice.DeviceUpdateSubject.DEVICE_STATE && (device.getState() == GBDevice.State.SCANNED)){
sendDeviceAPIBroadcast(device.getAddress(), API_LEGACY_ACTION_DEVICE_SCANNED);
}
}else if(BLEScanService.EVENT_DEVICE_FOUND.equals(action)){
String deviceAddress = intent.getStringExtra(BLEScanService.EXTRA_DEVICE_ADDRESS);
@ -449,14 +452,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
target.setState(GBDevice.State.SCANNED);
target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.CONNECTION_STATE);
target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.DEVICE_STATE);
new Handler().postDelayed(() -> {
if(target.getState() != GBDevice.State.SCANNED){
return;
}
deviceLastScannedTimestamps.put(target.getAddress(), System.currentTimeMillis());
target.setState(GBDevice.State.WAITING_FOR_SCAN);
target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.CONNECTION_STATE);
target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.DEVICE_STATE);
}, timeoutSeconds * 1000);
return;
}
@ -508,7 +511,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
ContextCompat.registerReceiver(this, mAutoConnectInvervalReceiver, new IntentFilter("GB_RECONNECT"), ContextCompat.RECEIVER_EXPORTED);
IntentFilter bluetoothCommandFilter = new IntentFilter();
bluetoothCommandFilter.addAction(COMMAND_BLUETOOTH_CONNECT);
bluetoothCommandFilter.addAction(API_LEGACY_COMMAND_BLUETOOTH_CONNECT);
ContextCompat.registerReceiver(this, bluetoothCommandReceiver, bluetoothCommandFilter, ContextCompat.RECEIVER_EXPORTED);
final IntentFilter deviceSettingsIntentFilter = new IntentFilter();
@ -557,7 +560,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
createDeviceStruct(device);
device.setState(GBDevice.State.WAITING_FOR_SCAN);
device.sendDeviceUpdateIntent(this);
device.sendDeviceUpdateIntent(this, GBDevice.DeviceUpdateSubject.DEVICE_STATE);
}
}

View File

@ -17,11 +17,13 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,6 +68,8 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; //this is common for all BTLE devices. see http://stackoverflow.com/questions/18699251/finding-out-android-bluetooth-le-gatt-profiles
private final Object characteristicsMonitor = new Object();
private BleIntentApi bleApi = null;
public AbstractBTLEDeviceSupport(Logger logger) {
this.logger = logger;
if (logger == null) {
@ -77,11 +81,15 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
public boolean connect() {
if (mQueue == null) {
mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, this, getContext(), mSupportedServerServices);
if(bleApi != null) {
bleApi.setQueue(mQueue);
}
mQueue.setAutoReconnect(getAutoReconnect());
mQueue.setScanReconnect(getScanReconnect());
mQueue.setImplicitGattCallbackModify(getImplicitCallbackModify());
mQueue.setSendWriteRequestResponse(getSendWriteRequestResponse());
}
return mQueue.connect();
}
@ -91,6 +99,29 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
}
}
public BleIntentApi getBleApi() {
return bleApi;
}
@Override
public void onSendConfiguration(String config) {
if(bleApi != null) {
bleApi.onSendConfiguration(config);
}
}
@Override
public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
super.setContext(gbDevice, btAdapter, context);
if(BleIntentApi.isEnabled(gbDevice)) {
bleApi = new BleIntentApi(context, gbDevice);
bleApi.handleBLEApiPrefs();
}
}
/**
* Returns whether the gatt callback should be implicitly set to the one on the transaction,
* even if it was not set directly on the transaction. If true, the gatt callback will always
@ -139,6 +170,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
mQueue.dispose();
mQueue = null;
}
if(bleApi != null) {
bleApi.dispose();
}
}
public TransactionBuilder createTransactionBuilder(String taskName) {
@ -271,6 +306,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
Set<UUID> supportedServices = getSupportedServices();
Map<UUID, BluetoothGattCharacteristic> newCharacteristics = new HashMap<>();
for (BluetoothGattService service : discoveredGattServices) {
if(bleApi != null) {
bleApi.addService(service);
}
if (supportedServices.contains(service.getUuid())) {
logger.debug("discovered supported service: {}: {}", BleNamesResolver.resolveServiceName(service.getUuid().toString()), service.getUuid());
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
@ -322,12 +361,22 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
logger.warn("Services discovered, but device state is already " + getDevice().getState() + " for device: " + getDevice() + ", so ignoring");
return;
}
initializeDevice(createTransactionBuilder("Initializing device")).queue(getQueue());
TransactionBuilder builder = createTransactionBuilder("Initializing device");
if(bleApi != null) {
bleApi.initializeDevice(builder);
}
initializeDevice(builder).queue(getQueue());
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
if(bleApi != null) {
bleApi.onCharacteristicChanged(characteristic);
}
for (AbstractBleProfile<?> profile : mSupportedProfiles) {
if (profile.onCharacteristicRead(gatt, characteristic, status)) {
return true;
@ -370,6 +419,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
if(bleApi != null) {
bleApi.onCharacteristicChanged(characteristic);
}
for (AbstractBleProfile<?> profile : mSupportedProfiles) {
if (profile.onCharacteristicChanged(gatt, characteristic)) {
return true;

View File

@ -0,0 +1,241 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_KEY_DEVICE_BLE_API_DEVICE_STATE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREFS_KEY_DEVICE_BLE_API_PACKAGE;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class BleIntentApi {
private Context context;
GBDevice device;
BtLEQueue queue;
Logger logger;
private boolean intentApiEnabledDeviceState = false;
private boolean intentApiEnabledReadWrite= false;
private boolean intentApiEnabledNotifications= false;
private String intentApiPackage = "";
private boolean intentApiCharacteristicReceiverRegistered = false;
private boolean intentApiDeviceStateReceiverRegistered = false;
private String lastReportedState = null;
private final HashMap<String, BluetoothGattCharacteristic> characteristics = new HashMap<>();
public static final String BLE_API_COMMAND_READ = "nodomain.freeyourgadget.gadgetbridge.ble_api.commands.CHARACTERISTIC_READ";
public static final String BLE_API_COMMAND_WRITE = "nodomain.freeyourgadget.gadgetbridge.ble_api.commands.CHARACTERISTIC_WRITE";
public static final String BLE_API_EVENT_CHARACTERISTIC_CHANGED = "nodomain.freeyourgadget.gadgetbridge.ble_api.events.CHARACTERISTIC_CHANGED";
BroadcastReceiver intentApiReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
boolean isWrite = BLE_API_COMMAND_WRITE.equals(action);
boolean isRead = BLE_API_COMMAND_READ.equals(action);
if((!isWrite) && (!isRead)) {
return;
}
if (!concernsThisDevice(intent)) {
return;
}
if(!getDevice().getState().equalsOrHigherThan(GBDevice.State.INITIALIZED)) {
logger.error(String.format("BLE API: Device %s not initialized.", getDevice()));
return;
}
String uuid = intent.getStringExtra("EXTRA_CHARACTERISTIC_UUID");
if (StringUtils.isNullOrEmpty(uuid)) {
logger.error("BLE API: missing EXTRA_CHARACTERISTIC_UUID");
return;
}
String hexData = intent.getStringExtra("EXTRA_PAYLOAD");
if (hexData == null) {
logger.error("BLE API: missing EXTRA_PAYLOAD");
return;
}
BluetoothGattCharacteristic characteristic = characteristics.get(uuid);
if(characteristic == null) {
logger.error("Characteristic {} not found", uuid);
return;
}
if(isWrite) {
new TransactionBuilder("BLE API write")
.write(characteristic, StringUtils.hexToBytes(hexData))
.queue(getQueue());
return;
}
if(isRead) {
new TransactionBuilder("BLE API read")
.read(characteristic)
.queue(getQueue());
return;
}
}
};
public static boolean isEnabled(GBDevice device) {
Prefs devicePrefs = GBApplication.getDevicePrefs(device.getAddress());
boolean intentApiEnabledReadWrite = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE, false);
boolean intentApiEnabledNotifications = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY, false);
boolean intentApiEnabledDeviceState = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_STATE, false);
return intentApiEnabledReadWrite | intentApiEnabledNotifications | intentApiEnabledDeviceState;
}
public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic) {
if(!intentApiEnabledNotifications) {
return;
}
Intent intent = getBleApiIntent(BLE_API_EVENT_CHARACTERISTIC_CHANGED);
if(!StringUtils.isNullOrEmpty(intentApiPackage)) {
intent.setPackage(intentApiPackage);
}
intent.putExtra("EXTRA_CHARACTERISTIC", characteristic.getUuid().toString());
intent.putExtra("EXTRA_PAYLOAD", StringUtils.bytesToHex(characteristic.getValue()));
getContext().sendBroadcast(intent);
}
public void initializeDevice(TransactionBuilder builder) {
if(intentApiEnabledNotifications) {
for (BluetoothGattCharacteristic characteristic : characteristics.values()) {
builder.notify(characteristic, true);
}
}
}
public void dispose() {
registerBleApiCharacteristicReceivers(false);
}
public void onSendConfiguration(String config) {
if(StringUtils.isNullOrEmpty(config)) {
return;
}
if(config.startsWith("prefs_device_ble_api_")) {
// could subscribe here, but there is more setup to do than that...
// handleBLEApiPrefs();
GB.toast(
getContext().getString(R.string.toast_setting_requires_reconnect),
Toast.LENGTH_SHORT,
GB.INFO
);
};
}
public Context getContext() {
return context;
}
public BtLEQueue getQueue() {
return queue;
}
public void setQueue(BtLEQueue queue) {
this.queue = queue;
}
private void registerBleApiCharacteristicReceivers(boolean enable){
if(enable == intentApiCharacteristicReceiverRegistered) {
return;
}
if(enable){
IntentFilter filter = new IntentFilter();
filter.addAction(BLE_API_COMMAND_READ);
filter.addAction(BLE_API_COMMAND_WRITE);
ContextCompat.registerReceiver(
getContext(),
intentApiReceiver,
filter,
ContextCompat.RECEIVER_EXPORTED
);
}else{
getContext().unregisterReceiver(intentApiReceiver);
}
intentApiCharacteristicReceiverRegistered = intentApiEnabledReadWrite;
}
public void addService(BluetoothGattService service) {
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
this.characteristics.put(characteristic.getUuid().toString(), characteristic);
}
}
public void handleBLEApiPrefs(){
Prefs devicePrefs = GBApplication.getDevicePrefs(getDevice().getAddress());
this.intentApiEnabledReadWrite = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE, false);
this.intentApiEnabledNotifications = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY, false);
this.intentApiEnabledDeviceState = devicePrefs.getBoolean(PREFS_KEY_DEVICE_BLE_API_DEVICE_STATE, false);
this.intentApiPackage = devicePrefs.getString(PREFS_KEY_DEVICE_BLE_API_PACKAGE, "");
registerBleApiCharacteristicReceivers(this.intentApiEnabledReadWrite);
}
public static Intent getBleApiIntent(String deviceAddress, String action) {
Intent updateIntent = new Intent(action);
updateIntent.putExtra("EXTRA_DEVICE_ADDRESS", deviceAddress);
return updateIntent;
}
private Intent getBleApiIntent(String action) {
return getBleApiIntent(getDevice().getAddress(), action);
}
public BleIntentApi(Context context, GBDevice device) {
this.context = context;
this.device = device;
this.logger = LoggerFactory.getLogger(BleIntentApi.class);
}
public GBDevice getDevice() {
return device;
}
private boolean concernsThisDevice(Intent intent) {
String deviceAddress = intent.getStringExtra("EXTRA_DEVICE_ADDRESS");
if (StringUtils.isNullOrEmpty(deviceAddress)) {
logger.error("BLE API: missing EXTRA_DEVICE_ADDRESS");
return false;
}
return deviceAddress.equalsIgnoreCase(getDevice().getAddress());
}
}

View File

@ -0,0 +1,51 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.gatt_client;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
public class BleGattClientCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public String getManufacturer() {
return "Generic";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return BleGattClientSupport.class;
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
// can only add through debug settings
return false;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_ble_gatt_client;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_scannable;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_scannable_disabled;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
}

View File

@ -0,0 +1,71 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.gatt_client;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
public class BleGattClientSupport extends AbstractBTLEDeviceSupport {
public static final Logger logger = LoggerFactory.getLogger(BleGattClientSupport.class);
public BleGattClientSupport() {
super(logger);
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if(characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = characteristic.getValue()[0];
handleGBDeviceEvent(batteryInfo);
}else if(characteristic.getUuid().equals(GattCharacteristic.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)) {
String firmwareVersion = characteristic.getStringValue(0);
getDevice().setFirmwareVersion(firmwareVersion);
getDevice().sendDeviceUpdateIntent(getContext());
}
return super.onCharacteristicRead(gatt, characteristic, status);
}
void readCharacteristicIfAvailable(UUID characteristicUUID, TransactionBuilder builder) {
BluetoothGattCharacteristic characteristic = getCharacteristic(characteristicUUID);
if(characteristic == null) {
return;
}
logger.debug("found characteristic {}", characteristicUUID);
builder.read(characteristic);
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
readCharacteristicIfAvailable(GattCharacteristic.UUID_CHARACTERISTIC_BATTERY_LEVEL, builder);
readCharacteristicIfAvailable(GattCharacteristic.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING, builder);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
return builder;
}
}

View File

@ -190,6 +190,20 @@ public class StringUtils {
return GB.hexdump(array, 0, -1);
}
public static byte[] hexToBytes(String hexString) {
if((hexString.length() % 2) == 1) {
// pad with zero
hexString = "0" + hexString;
}
byte[] bytes = new byte[hexString.length() / 2];
for(int i = 0; i < bytes.length; i++) {
String slice = hexString.substring(i * 2, i * 2 + 2);
bytes[i] = (byte) Integer.parseInt(slice, 16);
}
return bytes;
}
/**
* Creates a shortened version of an Android package name by using only the first
* character of every non-last part of the package name.

View File

@ -3255,4 +3255,13 @@
<string name="pref_header_deprecated_functionalities_warning">The following functionalities have been deprecated and will be removed soon from the software.\nIf you need to enable one of the following settings be sure to get in touch with the project team.</string>
<string name="pref_deprecated_media_control_title">Deprecated media control</string>
<string name="pref_deprecated_media_control_summary">Send media control commands as key events instead of the media controller.</string>
<string name="devicetype_ble_gatt_client">Generic BLE GATT Client</string>
<string name="prefs_title_gatt_client_notification_intents">Broadcast GATT notification Intents through BLE Intent API</string>
<string name="prefs_summary_gatt_client_notification_intents">Receive BLE characteristic changes through Intents</string>
<string name="prefs_title_gatt_client_allow_gatt_interactions">Allow GATT interaction through BLE Intent API</string>
<string name="prefs_summary_gatt_client_allow_gatt_interactions">Allow to send BLE characteristic read/write and connect commands</string>
<string name="prefs_summary_gatt_client_device_state_updates">Receive BLE connection state changes via Intents</string>
<string name="prefs_title_gatt_client_api_package">BLE API package</string>
<string name="prefs_summary_gatt_client_api_package">Restrict BLE Intent API communication to this package</string>
<string name="prefs_title_ble_intent_api">BLE Intent API</string>
</resources>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/prefs_title_ble_intent_api" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_bluetooth"
android:key="prefs_device_ble_api_characteristic_read_write"
android:layout="@layout/preference_checkbox"
android:title="@string/prefs_title_gatt_client_allow_gatt_interactions"
android:summary="@string/prefs_summary_gatt_client_allow_gatt_interactions"
android:defaultValue="false" />
<SwitchPreferenceCompat
android:icon="@drawable/ic_bluetooth"
android:key="prefs_device_ble_api_characteristic_notify"
android:layout="@layout/preference_checkbox"
android:title="@string/prefs_title_gatt_client_notification_intents"
android:summary="@string/prefs_summary_gatt_client_notification_intents"
android:defaultValue="false" />
<EditTextPreference
android:key="prefs_device_ble_api_package"
android:title="@string/prefs_title_gatt_client_api_package"
android:summary="@string/prefs_summary_gatt_client_api_package" />
</PreferenceScreen>