diff --git a/software/android/app/src/main/AndroidManifest.xml b/software/android/app/src/main/AndroidManifest.xml index f0af214..c721c7d 100644 --- a/software/android/app/src/main/AndroidManifest.xml +++ b/software/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + " + state) + mNewState = mState + + // Give the new state to the Handler so the UI Activity can update + mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget() + } + + /** + * Start the chat service. Specifically start AcceptThread to begin a + * session in listening (server) mode. Called by the Activity onResume() + */ + @Synchronized + fun start() { +// Log.d(TAG, "start") + + // Cancel any thread attempting to make a connection + if (mConnectThread != null) { + mConnectThread!!.cancel() + mConnectThread = null + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread!!.cancel() + mConnectedThread = null + } + + // Start the thread to listen on a BluetoothServerSocket + if (mSecureAcceptThread == null) { + mSecureAcceptThread = AcceptThread(true) + mSecureAcceptThread!!.start() + } + if (mInsecureAcceptThread == null) { + mInsecureAcceptThread = AcceptThread(false) + mInsecureAcceptThread!!.start() + } + // Update UI title + updateUserInterfaceTitle() + } + + /** + * Start the ConnectThread to initiate a connection to a remote device. + * + * @param device The BluetoothDevice to connect + * @param secure Socket Security type - Secure (true) , Insecure (false) + */ + @Synchronized + fun connect(device: BluetoothDevice, secure: Boolean) { +// Log.d(TAG, "connect to: $device") + + // Cancel any thread attempting to make a connection + if (mState == STATE_CONNECTING) { + if (mConnectThread != null) { + mConnectThread!!.cancel() + mConnectThread = null + } + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread!!.cancel() + mConnectedThread = null + } + + // Start the thread to connect with the given device + mConnectThread = ConnectThread(device, secure) + mConnectThread!!.start() + // Update UI title + updateUserInterfaceTitle() + } + + /** + * Start the ConnectedThread to begin managing a Bluetooth connection + * + * @param socket The BluetoothSocket on which the connection was made + * @param device The BluetoothDevice that has been connected + */ + @SuppressLint("MissingPermission") + @Synchronized + fun connected(socket: BluetoothSocket?, device: BluetoothDevice, socketType: String) { +// Log.d(TAG, "connected, Socket Type:$socketType") + + // Cancel the thread that completed the connection + if (mConnectThread != null) { + mConnectThread!!.cancel() + mConnectThread = null + } + + // Cancel any thread currently running a connection + if (mConnectedThread != null) { + mConnectedThread!!.cancel() + mConnectedThread = null + } + + // Cancel the accept thread because we only want to connect to one device + if (mSecureAcceptThread != null) { + mSecureAcceptThread!!.cancel() + mSecureAcceptThread = null + } + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread!!.cancel() + mInsecureAcceptThread = null + } + + // Start the thread to manage the connection and perform transmissions + mConnectedThread = ConnectedThread(socket, socketType) + mConnectedThread!!.start() + + // Send the name of the connected device back to the UI Activity + val msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME) + val bundle = Bundle() + bundle.putString(Constants.DEVICE_NAME, device.name) + msg.data = bundle + mHandler.sendMessage(msg) + // Update UI title + updateUserInterfaceTitle() + } + + /** + * Stop all threads + */ + @Synchronized + fun stop() { +// Log.d(TAG, "stop") + if (mConnectThread != null) { + mConnectThread!!.cancel() + mConnectThread = null + } + if (mConnectedThread != null) { + mConnectedThread!!.cancel() + mConnectedThread = null + } + if (mSecureAcceptThread != null) { + mSecureAcceptThread!!.cancel() + mSecureAcceptThread = null + } + if (mInsecureAcceptThread != null) { + mInsecureAcceptThread!!.cancel() + mInsecureAcceptThread = null + } + mState = STATE_NONE + // Update UI title + updateUserInterfaceTitle() + } + + /** + * Write to the ConnectedThread in an unsynchronized manner + * + * @param out The bytes to write + * @see ConnectedThread.write + */ + fun write(out: ByteArray?) { + // Create temporary object + var r: ConnectedThread? + // Synchronize a copy of the ConnectedThread + synchronized(this) { + if (mState != STATE_CONNECTED) return + r = mConnectedThread + } + // Perform the write unsynchronized + r!!.write(out) + } + + /** + * Indicate that the connection attempt failed and notify the UI Activity. + */ + private fun connectionFailed() { + // Send a failure message back to the Activity + val msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST) + val bundle = Bundle() + bundle.putString(Constants.TOAST, "Unable to connect device") + msg.data = bundle + mHandler.sendMessage(msg) + mState = STATE_NONE + // Update UI title + updateUserInterfaceTitle() + + // Start the service over to restart listening mode + start() + } + + /** + * Indicate that the connection was lost and notify the UI Activity. + */ + private fun connectionLost() { + // Send a failure message back to the Activity + val msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST) + val bundle = Bundle() + bundle.putString(Constants.TOAST, "Device connection was lost") + msg.data = bundle + mHandler.sendMessage(msg) + mState = STATE_NONE + // Update UI title + updateUserInterfaceTitle() + + // Start the service over to restart listening mode + start() + } + + /** + * This thread runs while listening for incoming connections. It behaves + * like a server-side client. It runs until a connection is accepted + * (or until cancelled). + */ + @SuppressLint("MissingPermission") + private inner class AcceptThread(secure: Boolean) : Thread() { + // The local server socket + private val mmServerSocket: BluetoothServerSocket? + private val mSocketType: String + override fun run() { +// Log.d( +// TAG, "Socket Type: " + mSocketType + +// "BEGIN mAcceptThread" + this +// ) + name = "AcceptThread$mSocketType" + var socket: BluetoothSocket? + + // Listen to the server socket if we're not connected + while (mState != STATE_CONNECTED) { + socket = try { + // This is a blocking call and will only return on a + // successful connection or an exception + mmServerSocket!!.accept() + } catch (e: IOException) { +// Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e) + break + } + + // If a connection was accepted + if (socket != null) { + synchronized(this@BluetoothChatService) { + when (mState) { + STATE_LISTEN, STATE_CONNECTING -> // Situation normal. Start the connected thread. + connected( + socket, socket.remoteDevice, + mSocketType + ) + STATE_NONE, STATE_CONNECTED -> // Either not ready or already connected. Terminate new socket. + try { + socket.close() + } catch (e: IOException) { +// Log.e( +// TAG, +// "Could not close unwanted socket", +// e +// ) + } + } + } + } + } +// Log.i( +// TAG, +// "END mAcceptThread, socket Type: $mSocketType" +// ) + } + + fun cancel() { +// Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this) + try { + mmServerSocket!!.close() + } catch (e: IOException) { +// Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e) + } + } + + init { + var tmp: BluetoothServerSocket? = null + mSocketType = if (secure) "Secure" else "Insecure" + + // Create a new listening server socket + try { + tmp = if (secure) { + mAdapter.listenUsingRfcommWithServiceRecord( + NAME_SECURE, + MY_UUID_SECURE + ) + } else { + mAdapter.listenUsingInsecureRfcommWithServiceRecord( + NAME_INSECURE, MY_UUID_INSECURE + ) + } + } catch (e: IOException) { +// Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e) + } + mmServerSocket = tmp + mState = STATE_LISTEN + } + } + + /** + * This thread runs while attempting to make an outgoing connection + * with a device. It runs straight through; the connection either + * succeeds or fails. + */ + @SuppressLint("MissingPermission") + private inner class ConnectThread(private val mmDevice: BluetoothDevice, secure: Boolean) : + Thread() { + private val mmSocket: BluetoothSocket? + private val mSocketType: String + override fun run() { +// Log.i( +// TAG, +// "BEGIN mConnectThread SocketType:$mSocketType" +// ) + name = "ConnectThread$mSocketType" + + // Always cancel discovery because it will slow down a connection + mAdapter.cancelDiscovery() + + // Make a connection to the BluetoothSocket + try { + // This is a blocking call and will only return on a + // successful connection or an exception + mmSocket!!.connect() + } catch (e: IOException) { + // Close the socket + try { + mmSocket!!.close() + } catch (e2: IOException) { +// Log.e( +// TAG, "unable to close() " + mSocketType + +// " socket during connection failure", e2 +// ) + } + connectionFailed() + return + } + + // Reset the ConnectThread because we're done + synchronized(this@BluetoothChatService) { mConnectThread = null } + + // Start the connected thread + connected(mmSocket, mmDevice, mSocketType) + } + + fun cancel() { + try { + mmSocket!!.close() + } catch (e: IOException) { +// Log.e( +// TAG, +// "close() of connect $mSocketType socket failed", e +// ) + } + } + + init { + var tmp: BluetoothSocket? = null + mSocketType = if (secure) "Secure" else "Insecure" + + // Get a BluetoothSocket for a connection with the + // given BluetoothDevice + try { + tmp = if (secure) { +// if (ActivityCompat.checkSelfPermission( +// this, +// Manifest.permission.BLUETOOTH_CONNECT +// ) != PackageManager.PERMISSION_GRANTED +// ) { +// // TODO: Consider calling +// // ActivityCompat#requestPermissions +// // here to request the missing permissions, and then overriding +// // public void onRequestPermissionsResult(int requestCode, String[] permissions, +// // int[] grantResults) +// // to handle the case where the user grants the permission. See the documentation +// // for ActivityCompat#requestPermissions for more details. +// return +// } + mmDevice.createRfcommSocketToServiceRecord( + MY_UUID_SECURE + ) + } else { + mmDevice.createInsecureRfcommSocketToServiceRecord( + MY_UUID_INSECURE + ) + } + } catch (e: IOException) { +// Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e) + } + mmSocket = tmp + mState = STATE_CONNECTING + } + } + + /** + * This thread runs during a connection with a remote device. + * It handles all incoming and outgoing transmissions. + */ + private inner class ConnectedThread(socket: BluetoothSocket?, socketType: String) : + Thread() { + private val mmSocket: BluetoothSocket? + private val mmInStream: InputStream? + private val mmOutStream: OutputStream? + override fun run() { +// Log.i(TAG, "BEGIN mConnectedThread") + val buffer = ByteArray(1024) + var bytes: Int + + // Keep listening to the InputStream while connected + while (mState == STATE_CONNECTED) { + try { + // Read from the InputStream + bytes = mmInStream!!.read(buffer) + + // Send the obtained bytes to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer) + .sendToTarget() + } catch (e: IOException) { +// Log.e(TAG, "disconnected", e) + connectionLost() + break + } + } + } + + /** + * Write to the connected OutStream. + * + * @param buffer The bytes to write + */ + fun write(buffer: ByteArray?) { + try { + mmOutStream!!.write(buffer) + + // Share the sent message back to the UI Activity + mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer) + .sendToTarget() + } catch (e: IOException) { +// Log.e(TAG, "Exception during write", e) + } + } + + fun cancel() { + try { + mmSocket!!.close() + } catch (e: IOException) { +// Log.e(TAG, "close() of connect socket failed", e) + } + } + + init { +// Log.d(TAG, "create ConnectedThread: $socketType") + mmSocket = socket + var tmpIn: InputStream? = null + var tmpOut: OutputStream? = null + + // Get the BluetoothSocket input and output streams + try { + tmpIn = socket!!.inputStream + tmpOut = socket.outputStream + } catch (e: IOException) { +// Log.e(TAG, "temp sockets not created", e) + } + mmInStream = tmpIn + mmOutStream = tmpOut + mState = STATE_CONNECTED + } + } + + companion object { + // Debugging + private const val TAG = "BluetoothChatService" + + // Name for the SDP record when creating server socket + private const val NAME_SECURE = "BluetoothChatSecure" + private const val NAME_INSECURE = "BluetoothChatInsecure" + + // Unique UUID for this application + private val MY_UUID_SECURE = UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66") + private val MY_UUID_INSECURE = UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66") + + // Constants that indicate the current connection state + const val STATE_NONE = 0 // we're doing nothing + const val STATE_LISTEN = 1 // now listening for incoming connections + const val STATE_CONNECTING = 2 // now initiating an outgoing connection + const val STATE_CONNECTED = 3 // now connected to a remote device + } + + /** + * Constructor. Prepares a new BluetoothChat session. + * + * @param context The UI Activity Context + * @param handler A Handler to send messages back to the UI Activity + */ + init { + mAdapter = BluetoothAdapter.getDefaultAdapter() + mState = STATE_NONE + mNewState = mState + mHandler = handler + } +} \ No newline at end of file diff --git a/software/android/app/src/main/java/com/rookiedev/hexapod/network/Constants.kt b/software/android/app/src/main/java/com/rookiedev/hexapod/network/Constants.kt new file mode 100644 index 0000000..6b45271 --- /dev/null +++ b/software/android/app/src/main/java/com/rookiedev/hexapod/network/Constants.kt @@ -0,0 +1,19 @@ +package com.rookiedev.hexapod.network + +/** + * Defines several constants used between [BluetoothChatService] and the UI. + */ +interface Constants { + companion object { + // Message types sent from the BluetoothChatService Handler + const val MESSAGE_STATE_CHANGE = 1 + const val MESSAGE_READ = 2 + const val MESSAGE_WRITE = 3 + const val MESSAGE_DEVICE_NAME = 4 + const val MESSAGE_TOAST = 5 + + // Key names received from the BluetoothChatService Handler + const val DEVICE_NAME = "device_name" + const val TOAST = "toast" + } +} \ No newline at end of file