/* Copyright (C) 2022-2024 Damien Gaignon, José Rebelo 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.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; import androidx.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; import java.util.Locale; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; public final class BtBRQueue { private static final Logger LOG = LoggerFactory.getLogger(BtBRQueue.class); public static final int HANDLER_SUBJECT_CONNECT = 0; public static final int HANDLER_SUBJECT_PERFORM_TRANSACTION = 1; private BluetoothAdapter mBtAdapter = null; private BluetoothSocket mBtSocket = null; private final GBDevice mGbDevice; private final SocketCallback mCallback; private final UUID mService; private volatile boolean mDisposed; private final Context mContext; private final int mBufferSize; private final Handler mWriteHandler; private final HandlerThread mWriteHandlerThread = new HandlerThread("Write Thread", Process.THREAD_PRIORITY_BACKGROUND); private Thread readThread = new Thread("Read Thread") { @Override public void run() { byte[] buffer = new byte[mBufferSize]; int nRead; LOG.debug("Read thread started, entering loop"); while (!mDisposed) { try { nRead = mBtSocket.getInputStream().read(buffer); // safety measure if (nRead == -1) { throw new IOException("End of stream"); } } catch (IOException ex) { LOG.error("IO exception while reading message from socket, breaking out of read thread: ", ex); break; } LOG.debug("Received {} bytes: {}", nRead, GB.hexdump(buffer, 0, nRead)); try { mCallback.onSocketRead(Arrays.copyOf(buffer, nRead)); } catch (Throwable ex) { LOG.error("Failed to process received bytes in onSocketRead callback: ", ex); } } LOG.debug("Exited read thread loop, disconnecting"); GBApplication.deviceService(mGbDevice).disconnect(); } }; 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; mWriteHandlerThread.start(); LOG.debug("Write handler thread is prepared, creating write handler"); mWriteHandler = new Handler(mWriteHandlerThread.getLooper()) { @SuppressLint("MissingPermission") @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case HANDLER_SUBJECT_CONNECT: { try { mBtSocket.connect(); LOG.info("Connected to RFCOMM socket for {}", mGbDevice.getName()); setDeviceConnectionState(GBDevice.State.CONNECTED); // update thread names to show device names in logs readThread.setName(String.format(Locale.ENGLISH, "Read Thread for %s", mGbDevice.getName())); mWriteHandlerThread.setName(String.format(Locale.ENGLISH, "Write Thread for %s", mGbDevice.getName())); // now that connect has been created, start the threads readThread.start(); onConnectionEstablished(); } catch (IOException e) { LOG.error("IO exception while establishing socket connection: ", e); setDeviceConnectionState(GBDevice.State.NOT_CONNECTED); } return; } case HANDLER_SUBJECT_PERFORM_TRANSACTION: { try { if (!isConnected()) { LOG.debug("Not connected, updating device state to WAITING_FOR_RECONNECT"); setDeviceConnectionState(GBDevice.State.WAITING_FOR_RECONNECT); return; } if (!(msg.obj instanceof Transaction)) { LOG.error("msg.obj is not an instance of Transaction"); return; } Transaction transaction = (Transaction) msg.obj; 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, cancelling further actions in transaction: {}", action); break; } } } catch (Throwable ex) { LOG.error("IO Write Thread died: " + ex.getMessage(), ex); } return; } } LOG.warn("Unhandled write handler message {}", msg.what); } }; } /** * 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 */ @SuppressLint("MissingPermission") public boolean connect() { if (isConnected()) { LOG.warn("Ignoring connect() because already connected."); return false; } LOG.info("Attempting to connect to {} ({})", mGbDevice.getName(), mGbDevice.getAddress()); // stop discovery before connection is made mBtAdapter.cancelDiscovery(); // revert to original state upon exception GBDevice.State originalState = mGbDevice.getState(); setDeviceConnectionState(GBDevice.State.CONNECTING); try { BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(mGbDevice.getAddress()); mBtSocket = btDevice.createRfcommSocketToServiceRecord(mService); } catch (IOException e) { LOG.error("Unable to connect to RFCOMM endpoint: ", e); setDeviceConnectionState(originalState); mBtSocket = null; return false; } LOG.debug("Socket created, connecting in handler"); mWriteHandler.sendMessageAtFrontOfQueue(mWriteHandler.obtainMessage(HANDLER_SUBJECT_CONNECT)); return true; } protected void onConnectionEstablished() { mCallback.onConnectionEstablished(); } public void disconnect() { if (mWriteHandlerThread.isAlive()) { mWriteHandlerThread.quit(); } if (mBtSocket != null && mBtSocket.isConnected()) { try { mBtSocket.close(); } catch (IOException e) { LOG.error("IO exception while closing socket in disconnect(): ", e); } } mBtSocket = null; setDeviceConnectionState(GBDevice.State.NOT_CONNECTED); } /** * Check whether a connection to the device exists and whether a socket connection has been * initialized and connected * @return true if the Bluetooth device is connected and the socket is ready, false otherwise */ private boolean isConnected() { return mGbDevice.isConnected() && mBtSocket != null && mBtSocket.isConnected(); } /** * Add a finalized {@link Transaction} to the write handler's queue * * @param transaction The transaction to be run in the handler thread's looper */ public void add(Transaction transaction) { LOG.debug("Adding transaction to looper message queue: {}", transaction); if (!transaction.isEmpty()) { mWriteHandler.obtainMessage(HANDLER_SUBJECT_PERFORM_TRANSACTION, transaction).sendToTarget(); } } private 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(); if (readThread != null && readThread.isAlive()) { readThread.interrupt(); readThread = null; } } }