BT Classic/BTBR: Fix concurrency issues on connection

This commit is contained in:
José Rebelo
2025-07-03 19:59:00 +02:00
committed by José Rebelo
parent c34885974a
commit 8916555044
18 changed files with 103 additions and 133 deletions
@@ -25,7 +25,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDeviceSupport implements HeadphoneHelper.Callback { public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDeviceSupport implements HeadphoneHelper.Callback {
private HeadphoneHelper headphoneHelper; private HeadphoneHelper headphoneHelper;
@Override @Override
@@ -44,12 +43,6 @@ public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDevic
headphoneHelper = new HeadphoneHelper(getContext(), getDevice(), this); headphoneHelper = new HeadphoneHelper(getContext(), getDevice(), this);
} }
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public void onSendConfiguration(String config) { public void onSendConfiguration(String config) {
if (!headphoneHelper.onSendConfiguration(config)) if (!headphoneHelper.onSendConfiguration(config))
@@ -58,8 +51,10 @@ public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDevic
@Override @Override
public void dispose() { public void dispose() {
if (headphoneHelper != null) synchronized (ConnectionMonitor) {
headphoneHelper.dispose(); if (headphoneHelper != null)
super.dispose(); headphoneHelper.dispose();
super.dispose();
}
} }
} }
@@ -36,6 +36,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
* @see nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread * @see nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread
*/ */
public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport implements SocketCallback { public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport implements SocketCallback {
/// used to guard {@link #connect()}, {@link #disconnect()} and {@link #dispose()}
protected final Object ConnectionMonitor = new Object();
private BtBRQueue mQueue; private BtBRQueue mQueue;
private UUID mSupportedService = null; private UUID mSupportedService = null;
private int mBufferSize = 1024; private int mBufferSize = 1024;
@@ -50,20 +54,24 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
@Override @Override
public boolean connect() { public boolean connect() {
final UUID supportedService = getSupportedService(); synchronized (ConnectionMonitor) {
if (supportedService == null) { final UUID supportedService = getSupportedService();
throw new NullPointerException("No supported service UUID specified"); if (supportedService == null) {
} throw new NullPointerException("No supported service UUID specified");
}
if (mQueue == null) { if (mQueue == null) {
mQueue = new BtBRQueue(getBluetoothAdapter(), getDevice(), getContext(), this, supportedService, getBufferSize()); mQueue = new BtBRQueue(getBluetoothAdapter(), getDevice(), getContext(), this, supportedService, getBufferSize());
}
return mQueue.connect();
} }
return mQueue.connect();
} }
public void disconnect() { public void disconnect() {
if (mQueue != null) { synchronized (ConnectionMonitor) {
mQueue.disconnect(); if (mQueue != null) {
mQueue.disconnect();
}
} }
} }
@@ -80,9 +88,11 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
@Override @Override
public void dispose() { public void dispose() {
if (mQueue != null) { synchronized (ConnectionMonitor) {
mQueue.dispose(); if (mQueue != null) {
mQueue = null; mQueue.dispose();
mQueue = null;
}
} }
} }
@@ -91,7 +101,7 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
} }
@Override @Override
public boolean isConnected(){ public boolean isConnected() {
// in a multi-threaded environment the queue knows // in a multi-threaded environment the queue knows
// best about the up-to-date connection status // best about the up-to-date connection status
return (mQueue != null) && mQueue.isConnected(); return (mQueue != null) && mQueue.isConnected();
@@ -33,7 +33,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -171,13 +170,11 @@ public final class BtBRQueue {
return; return;
} }
if (!(msg.obj instanceof Transaction)) { if (!(msg.obj instanceof Transaction transaction)) {
LOG.error("msg.obj is not an instance of Transaction"); LOG.error("msg.obj is not an instance of Transaction");
return; return;
} }
Transaction transaction = (Transaction) msg.obj;
for (BtBRAction action : transaction.getActions()) { for (BtBRAction action : transaction.getActions()) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("About to run action: {}", action); LOG.debug("About to run action: {}", action);
@@ -212,8 +209,15 @@ public final class BtBRQueue {
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
public boolean connect() { public boolean connect() {
if (isConnected()) { final GBDevice.State state = mGbDevice.getState();
LOG.warn("Ignoring connect() because already connected."); if (state.equalsOrHigherThan(GBDevice.State.CONNECTING)) {
LOG.warn("connect - ignored, state is {}", state);
return false;
} else if (mBtSocket != null) {
LOG.warn("connect - ignored, mBtSocket isn't null");
return false;
} else if (mDisposed.get()) {
LOG.error("connect - ignored, this BtBRQueue has already been disposed");
return false; return false;
} }
@@ -304,6 +308,7 @@ public final class BtBRQueue {
public void dispose() { public void dispose() {
if (mDisposed.getAndSet(true)) { if (mDisposed.getAndSet(true)) {
LOG.warn("dispose() was called repeatedly");
return; return;
} }
@@ -101,13 +101,15 @@ public class AAWirelessSupport extends AbstractBTBRDeviceSupport {
@Override @Override
public void dispose() { public void dispose() {
try { synchronized (ConnectionMonitor) {
getContext().unregisterReceiver(commandReceiver); try {
} catch (final Exception e) { getContext().unregisterReceiver(commandReceiver);
LOG.warn("Failed to unregister receiver", e); } catch (final Exception e) {
} LOG.warn("Failed to unregister receiver", e);
}
super.dispose(); super.dispose();
}
} }
@Override @Override
@@ -19,28 +19,16 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.divoom;
import android.net.Uri; import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class PixooSupport extends AbstractSerialDeviceSupport { public class PixooSupport extends AbstractSerialDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(PixooSupport.class);
@Override @Override
public void onSendConfiguration(String config) { public void onSendConfiguration(String config) {
super.onSendConfiguration(config); super.onSendConfiguration(config);
} }
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public synchronized PixooIOThread getDeviceIOThread() { public synchronized PixooIOThread getDeviceIOThread() {
return (PixooIOThread) super.getDeviceIOThread(); return (PixooIOThread) super.getDeviceIOThread();
@@ -117,9 +117,11 @@ public class ZeppOsBtbrSupport extends AbstractBTBRDeviceSupport implements Zepp
@Override @Override
public void dispose() { public void dispose() {
zeppOsSupport.dispose(); synchronized (ConnectionMonitor) {
pingHandler.removeCallbacksAndMessages(null); zeppOsSupport.dispose();
super.dispose(); pingHandler.removeCallbacksAndMessages(null);
super.dispose();
}
} }
@Override @Override
@@ -199,8 +199,10 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
@Override @Override
public void dispose() { public void dispose() {
supportProvider.dispose(); synchronized (ConnectionMonitor) {
super.dispose(); supportProvider.dispose();
super.dispose();
}
} }
@Override @Override
@@ -108,9 +108,11 @@ public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneH
@Override @Override
public void dispose() { public void dispose() {
if (headphoneHelper != null) synchronized (ConnectionMonitor) {
headphoneHelper.dispose(); if (headphoneHelper != null)
super.dispose(); headphoneHelper.dispose();
super.dispose();
}
} }
@Override @Override
@@ -17,29 +17,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview; package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
import android.net.Uri;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class LiveviewSupport extends AbstractSerialDeviceSupport { public class LiveviewSupport extends AbstractSerialDeviceSupport {
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
protected GBDeviceProtocol createDeviceProtocol() { protected GBDeviceProtocol createDeviceProtocol() {
return new LiveviewProtocol(getDevice()); return new LiveviewProtocol(getDevice());
@@ -59,9 +41,4 @@ public class LiveviewSupport extends AbstractSerialDeviceSupport {
public synchronized LiveviewIoThread getDeviceIOThread() { public synchronized LiveviewIoThread getDeviceIOThread() {
return (LiveviewIoThread) super.getDeviceIOThread(); return (LiveviewIoThread) super.getDeviceIOThread();
} }
@Override
public void onNotification(NotificationSpec notificationSpec) {
super.onNotification(notificationSpec);
}
} }
@@ -55,8 +55,10 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
@Override @Override
public void dispose() { public void dispose() {
super.dispose(); synchronized (ConnectionMonitor) {
unregisterSunriseSunsetAlarmReceiver(); super.dispose();
unregisterSunriseSunsetAlarmReceiver();
}
} }
private void registerSunriseSunsetAlarmReceiver() { private void registerSunriseSunsetAlarmReceiver() {
@@ -79,8 +81,13 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
@Override @Override
public boolean connect() { public boolean connect() {
getDeviceIOThread().start(); synchronized (ConnectionMonitor) {
registerSunriseSunsetAlarmReceiver(); final PebbleIoThread deviceIOThread = getDeviceIOThread();
if (!deviceIOThread.isAlive()) {
deviceIOThread.start();
}
registerSunriseSunsetAlarmReceiver();
}
return true; return true;
} }
@@ -5,13 +5,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class PixelBudsADeviceSupport extends AbstractSerialDeviceSupport { public class PixelBudsADeviceSupport extends AbstractSerialDeviceSupport {
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -21,14 +21,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class QC35BaseSupport extends AbstractSerialDeviceSupport { public class QC35BaseSupport extends AbstractSerialDeviceSupport {
@Override
public boolean connect() {
getDeviceProtocol();
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -33,12 +33,6 @@ public class RedmiBudsDeviceSupport extends AbstractSerialDeviceSupport {
RedmiBudsDeviceSupport.this, getBluetoothAdapter()); RedmiBudsDeviceSupport.this, getBluetoothAdapter());
} }
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -79,11 +79,14 @@ public class RoidmiSupport extends AbstractSerialDeviceSupport {
@Override @Override
public boolean connect() { public boolean connect() {
getDeviceIOThread().start(); synchronized (ConnectionMonitor) {
final RoidmiIoThread deviceIOThread = getDeviceIOThread();
requestDeviceInfos(1500); if (!deviceIOThread.isAlive()) {
deviceIOThread.start();
return true; requestDeviceInfos(1500);
}
return true;
}
} }
@Override @Override
@@ -104,7 +107,7 @@ public class RoidmiSupport extends AbstractSerialDeviceSupport {
@Override @Override
public void onSendConfiguration(final String config) { public void onSendConfiguration(final String config) {
LOG.debug("onSendConfiguration " + config); LOG.debug("onSendConfiguration {}", config);
final RoidmiIoThread roidmiIoThread = getDeviceIOThread(); final RoidmiIoThread roidmiIoThread = getDeviceIOThread();
final RoidmiProtocol roidmiProtocol = (RoidmiProtocol) getDeviceProtocol(); final RoidmiProtocol roidmiProtocol = (RoidmiProtocol) getDeviceProtocol();
@@ -120,7 +123,7 @@ public class RoidmiSupport extends AbstractSerialDeviceSupport {
roidmiIoThread.write(roidmiProtocol.encodeGetVoltage()); roidmiIoThread.write(roidmiProtocol.encodeGetVoltage());
break; break;
default: default:
LOG.error("Invalid Roidmi configuration " + config); LOG.error("Invalid Roidmi configuration {}", config);
break; break;
} }
} }
@@ -9,12 +9,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreLiberty3ProDeviceSupport extends AbstractSerialDeviceSupport { public class SoundcoreLiberty3ProDeviceSupport extends AbstractSerialDeviceSupport {
public static final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3952"); public static final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3952");
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -9,12 +9,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreLiberty4NCDeviceSupport extends AbstractSerialDeviceSupport { public class SoundcoreLiberty4NCDeviceSupport extends AbstractSerialDeviceSupport {
public static final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3947"); public static final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3947");
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -16,12 +16,6 @@ public class SoundcoreQ30DeviceSupport extends AbstractSerialDeviceSupport {
return new SoundcoreQ30IOThread(getDevice(), getContext(), (SoundcoreQ30Protocol) getDeviceProtocol(),this, getBluetoothAdapter()); return new SoundcoreQ30IOThread(getDevice(), getContext(), (SoundcoreQ30Protocol) getDeviceProtocol(),this, getBluetoothAdapter());
} }
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override @Override
public boolean useAutoConnect() { public boolean useAutoConnect() {
return false; return false;
@@ -56,6 +56,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport { public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(AbstractSerialDeviceSupport.class); private static final Logger LOG = LoggerFactory.getLogger(AbstractSerialDeviceSupport.class);
/// used to guard {@link #connect()} and {@link #dispose()}
protected final Object ConnectionMonitor = new Object();
private GBDeviceProtocol gbDeviceProtocol; private GBDeviceProtocol gbDeviceProtocol;
protected GBDeviceIoThread gbDeviceIOThread; protected GBDeviceIoThread gbDeviceIOThread;
@@ -69,13 +72,26 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport
*/ */
protected abstract GBDeviceIoThread createDeviceIOThread(); protected abstract GBDeviceIoThread createDeviceIOThread();
@Override
public boolean connect() {
synchronized (ConnectionMonitor) {
final GBDeviceIoThread deviceIOThread = getDeviceIOThread();
if (!deviceIOThread.isAlive()) {
deviceIOThread.start();
}
return true;
}
}
@Override @Override
public void dispose() { public void dispose() {
// currently only one thread allowed synchronized (ConnectionMonitor) {
if (gbDeviceIOThread != null) { // currently only one thread allowed
gbDeviceIOThread.quit(); if (gbDeviceIOThread != null) {
gbDeviceIOThread.interrupt(); gbDeviceIOThread.quit();
gbDeviceIOThread = null; gbDeviceIOThread.interrupt();
gbDeviceIOThread = null;
}
} }
} }