/* Copyright (C) 2022 Damien Gaignon * * This file is part of Gadgetbridge. * * Gadgetbridge is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Gadgetbridge is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.btbr; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.ParcelUuid; import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public final class BtBRQueue { private static final Logger LOG = LoggerFactory.getLogger(BtBRQueue.class); private BluetoothAdapter mBtAdapter = null; private BluetoothSocket mBtSocket = null; private GBDevice mGbDevice; private SocketCallback mCallback; private UUID mService; private final BlockingQueue mTransactions = new LinkedBlockingQueue<>(); private volatile boolean mDisposed; private volatile boolean mCrashed; private Context mContext; private CountDownLatch mConnectionLatch; private CountDownLatch mAvailableData; private int mBufferSize; private Thread writeThread = new Thread("Gadgetbridge IO writeThread") { @Override public void run() { LOG.debug("Socket Write Thread started."); while (!mDisposed && !mCrashed) { try { AbstractTransaction qTransaction = mTransactions.take(); if (!isConnected()) { LOG.debug("Not connected, waiting for connection..."); setDeviceConnectionState(GBDevice.State.NOT_CONNECTED); // wait until the connection succeeds before running the actions // Note that no automatic connection is performed. This has to be triggered // on the outside typically by the DeviceSupport. The reason is that // devices have different kinds of initializations and this class has no // idea about them. mConnectionLatch = new CountDownLatch(1); mConnectionLatch.await(); mConnectionLatch = null; } LOG.info("Ready for a new message exchange."); Transaction transaction = (Transaction)qTransaction; for (BtBRAction action : transaction.getActions()) { if (LOG.isDebugEnabled()) { LOG.debug("About to run action: " + action); } if (action.run(mBtSocket)) { LOG.debug("Action ok: " + action); } else { LOG.error("Action returned false: " + action); break; } } } catch (InterruptedException ignored) { mConnectionLatch = null; LOG.debug("Thread interrupted"); } catch (Throwable ex) { LOG.error("IO Write Thread died: " + ex.getMessage(), ex); mCrashed = true; mConnectionLatch = null; } } } }; private Thread readThread = new Thread("Gadgetbridge IO readThread") { @Override public void run() { LOG.debug("Queue Read Thread started."); while (!mDisposed && !mCrashed) { try { if (!isConnected()) { LOG.debug("not connected, waiting for connection..."); // wait until the connection succeeds before running the actions // Note that no automatic connection is performed. This has to be triggered // on the outside typically by the DeviceSupport. The reason is that // devices have different kinds of initializations and this class has no // idea about them. mConnectionLatch = new CountDownLatch(1); mConnectionLatch.await(); mConnectionLatch = null; } if (mAvailableData != null) { if (mBtSocket.getInputStream().available() == 0) { mAvailableData.countDown(); } } byte[] data = new byte[mBufferSize]; int len = mBtSocket.getInputStream().read(data); LOG.debug("Received data: " + StringUtils.bytesToHex(data)); mCallback.onSocketRead(Arrays.copyOf(data, len)); } catch (InterruptedException ignored) { mConnectionLatch = null; LOG.debug("Thread interrupted"); } catch (Throwable ex) { LOG.error("IO Read Thread died: " + ex.getMessage(), ex); mCrashed = true; mConnectionLatch = null; } } } }; public BtBRQueue(BluetoothAdapter btAdapter, GBDevice gbDevice, Context context, SocketCallback socketCallback, UUID supportedService, int bufferSize) { mBtAdapter = btAdapter; mGbDevice = gbDevice; mContext = context; mCallback = socketCallback; mService = supportedService; mBufferSize = bufferSize; writeThread.start(); readThread.start(); } /** * Connects to the given remote device. Note that this does not perform any device * specific initialization. This should be done in the specific {@link DeviceSupport} * class. * * @return true whether the connection attempt was successfully triggered and false if that failed or if there is already a connection */ protected boolean connect() { if (isConnected()) { LOG.warn("Ignoring connect() because already connected."); return false; } LOG.info("Attemping to connect to " + mGbDevice.getName()); GBDevice.State originalState = mGbDevice.getState(); setDeviceConnectionState(GBDevice.State.CONNECTING); try { BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(mGbDevice.getAddress()); // UUID should be in a BluetoothSocket class and not in BluetoothSocketCharacteristic mBtSocket = btDevice.createRfcommSocketToServiceRecord(mService); mBtSocket.connect(); if (mBtSocket.isConnected()) { setDeviceConnectionState(GBDevice.State.CONNECTED); } else { LOG.debug("Connection not established"); } if (mConnectionLatch != null) { mConnectionLatch.countDown(); } } catch (IOException e) { LOG.error("Server socket cannot be started.", e); setDeviceConnectionState(originalState); mBtSocket = null; return false; } onConnectionEstablished(); return true; } protected void onConnectionEstablished() { mCallback.onConnectionEstablished(); } public void disconnect() { if (mBtSocket != null) { try { mAvailableData = new CountDownLatch(1); mAvailableData.await(); mAvailableData = null; mBtSocket.close(); } catch (IOException e) { LOG.error(e.getMessage()); } catch (InterruptedException e) { LOG.error(e.getMessage()); } } } protected boolean isConnected() { return mGbDevice.isConnected(); } /** * Adds a transaction to the end of the queue. * * @param transaction */ public void add(Transaction transaction) { LOG.debug("about to add: " + transaction); if (!transaction.isEmpty()) { mTransactions.add(transaction); } } protected void setDeviceConnectionState(GBDevice.State newState) { LOG.debug("New device connection state: " + newState); mGbDevice.setState(newState); mGbDevice.sendDeviceUpdateIntent(mContext, GBDevice.DeviceUpdateSubject.CONNECTION_STATE); } public void dispose() { if (mDisposed) { return; } mDisposed = true; disconnect(); writeThread.interrupt(); writeThread = null; readThread.interrupt(); readThread = null; } }