Garmin/app/src/main/java/eu/ztsh/garmin/bt/BluetoothSerial.kt

310 lines
12 KiB
Kotlin

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<Void?, Void?, BluetoothDevice?>? = 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>(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<BluetoothDevice> = ArrayList(bluetoothAdapter.getBondedDevices())
if (pairedDevices.isNotEmpty()) {
bluetoothAdapter.cancelDiscovery()
/**
* AsyncTask to handle the establishing of a bluetooth connection
*/
// connectionTask = object : AsyncTask<Void?, Void?, BluetoothDevice?>() {
// 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<Class<*>?>(
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
}
}