package eu.ztsh.garmin.bt import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.AsyncTask import android.util.Log import androidx.localbroadcastmanager.content.LocalBroadcastManager import eu.ztsh.garmin.MainActivity import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.util.* class BluetoothSerial(var context: MainActivity, var messageHandler: MessageHandler, devicePrefix: String) { var connected = false var bluetoothDevice: BluetoothDevice? = null var serialSocket: BluetoothSocket? = null var serialInputStream: InputStream? = null var serialOutputStream: OutputStream? = null var serialReader: SerialReader? = null var connectionTask: AsyncTask? = null var devicePrefix: String /** * Listens for discount message from bluetooth system and restablishing a connection */ private val bluetoothReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.action val eventDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) { if (bluetoothDevice != null && bluetoothDevice == eventDevice) { Log.i(BMX_BLUETOOTH, "Received bluetooth disconnect notice") //clean up any streams close() //reestablish connect connect() LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(BLUETOOTH_DISCONNECTED)) } } } } init { this.devicePrefix = devicePrefix.uppercase(Locale.getDefault()) } fun onPause() { context.unregisterReceiver(bluetoothReceiver) } fun onResume() { //listen for bluetooth disconnect val disconnectIntent = IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED) context.registerReceiver(bluetoothReceiver, disconnectIntent) //reestablishes a connection is one doesn't exist if (!connected) { connect() } else { val intent = Intent(BLUETOOTH_CONNECTED) LocalBroadcastManager.getInstance(context).sendBroadcast(intent) } } /** * Initializes the bluetooth serial connections, uses the LocalBroadcastManager when * connection is established */ @SuppressLint("MissingPermission") fun connect() { if (connected) { Log.e(BMX_BLUETOOTH, "Connection request while already connected") return } if (connectionTask != null && connectionTask!!.status == AsyncTask.Status.RUNNING) { Log.e(BMX_BLUETOOTH, "Connection request while attempting connection") return } val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) { return } context.checkBt() val pairedDevices: List = ArrayList(bluetoothAdapter.getBondedDevices()) if (pairedDevices.isNotEmpty()) { bluetoothAdapter.cancelDiscovery() /** * AsyncTask to handle the establishing of a bluetooth connection */ // connectionTask = object : AsyncTask() { // var MAX_ATTEMPTS = 30 // var attemptCounter = 0 // protected override fun doInBackground(vararg params: Void): BluetoothDevice? { // while (!isCancelled) { //need to kill without calling onCancel // for (device in pairedDevices) { // if (device.getName().uppercase(Locale.getDefault()).startsWith(devicePrefix)) { // Log.i( // BMX_BLUETOOTH, // attemptCounter.toString() + ": Attempting connection to " + device.getName() // ) // try { // serialSocket = try { // // Standard SerialPortService ID // val uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") // device.createRfcommSocketToServiceRecord(uuid) // } catch (ce: Exception) { // connectViaReflection(device) // } // // //setup the connect streams // serialSocket!!.connect() // serialInputStream = serialSocket!!.inputStream // serialOutputStream = serialSocket!!.outputStream // connected = true // Log.i(BMX_BLUETOOTH, "Connected to " + device.getName()) // return device // } catch (e: Exception) { // serialSocket = null // serialInputStream = null // serialOutputStream = null // Log.i(BMX_BLUETOOTH, e.message!!) // } // } // } // try { // attemptCounter++ // if (attemptCounter > MAX_ATTEMPTS) cancel(false) else Thread.sleep(1000) // } catch (e: InterruptedException) { // break // } // } // Log.i(BMX_BLUETOOTH, "Stopping connection attempts") // val intent = Intent(BLUETOOTH_FAILED) // LocalBroadcastManager.getInstance(context).sendBroadcast(intent) // return null // } // // override fun onPostExecute(result: BluetoothDevice?) { // super.onPostExecute(result) // bluetoothDevice = result // // //start thread responsible for reading from inputstream // serialReader = SerialReader() // serialReader!!.start() // // //send connection message // val intent = Intent(BLUETOOTH_CONNECTED) // LocalBroadcastManager.getInstance(context).sendBroadcast(intent) // } // } // connectionTask.execute() } } // see: http://stackoverflow.com/questions/3397071/service-discovery-failed-exception-using-bluetooth-on-android @Throws(Exception::class) private fun connectViaReflection(device: BluetoothDevice): BluetoothSocket { val m = device.javaClass.getMethod( "createRfcommSocket", *arrayOf?>( Int::class.javaPrimitiveType ) ) return m.invoke(device, 1) as BluetoothSocket } @Throws(IOException::class) fun available(): Int { if (connected) return serialInputStream!!.available() throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun read(): Int { if (connected) return serialInputStream!!.read() throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun read(buffer: ByteArray?): Int { if (connected) return serialInputStream!!.read(buffer) throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun read(buffer: ByteArray?, byteOffset: Int, byteCount: Int): Int { if (connected) return serialInputStream!!.read(buffer, byteOffset, byteCount) throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun write(buffer: ByteArray?) { if (connected) serialOutputStream!!.write(buffer) throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun write(oneByte: Int) { if (connected) serialOutputStream!!.write(oneByte) throw RuntimeException("Connection lost, reconnecting now.") } @Throws(IOException::class) fun write(buffer: ByteArray?, offset: Int, count: Int) { serialOutputStream!!.write(buffer, offset, count) throw RuntimeException("Connection lost, reconnecting now.") } inner class SerialReader : Thread() { var buffer = ByteArray(Companion.MAX_BYTES) var bufferSize = 0 override fun run() { Log.i("serialReader", "Starting serial loop") while (!isInterrupted) { try { /* * check for some bytes, or still bytes still left in * buffer */ if (available() > 0) { val newBytes = read(buffer, bufferSize, Companion.MAX_BYTES - bufferSize) if (newBytes > 0) bufferSize += newBytes Log.d(BMX_BLUETOOTH, "read $newBytes") } if (bufferSize > 0) { val read = messageHandler.read(bufferSize, buffer) // shift unread data to start of buffer if (read > 0) { var index = 0 for (i in read until bufferSize) { buffer[index++] = buffer[i] } bufferSize = index } } else { try { sleep(10) } catch (ie: InterruptedException) { break } } } catch (e: Exception) { Log.e(BMX_BLUETOOTH, "Error reading serial data", e) } } Log.i(BMX_BLUETOOTH, "Shutting serial loop") } // companion object { // } } /** * Reads from the serial buffer, processing any available messages. Must return the number of bytes * consumer from the buffer * * @author jpetrocik */ fun interface MessageHandler { fun read(bufferSize: Int, buffer: ByteArray?): Int } fun close() { connected = false if (connectionTask != null) { connectionTask!!.cancel(false) } if (serialReader != null) { serialReader!!.interrupt() try { serialReader!!.join(1000) } catch (ie: InterruptedException) { } } try { serialInputStream!!.close() } catch (e: Exception) { Log.e(BMX_BLUETOOTH, "Failed releasing inputstream connection") } try { serialOutputStream!!.close() } catch (e: Exception) { Log.e(BMX_BLUETOOTH, "Failed releasing outputstream connection") } try { serialSocket!!.close() } catch (e: Exception) { Log.e(BMX_BLUETOOTH, "Failed closing socket") } Log.i(BMX_BLUETOOTH, "Released bluetooth connections") } companion object { private const val BMX_BLUETOOTH = "BMXBluetooth" var BLUETOOTH_CONNECTED = "bluetooth-connection-started" var BLUETOOTH_DISCONNECTED = "bluetooth-connection-lost" var BLUETOOTH_FAILED = "bluetooth-connection-failed" private const val MAX_BYTES = 125 } }