diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a1b4c48e96c7826ef8d6d6ea2931fb3e2e9fabfe..cfb44a5e811d1a67233e27733b1f0d9599a1018b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ </activity> <activity android:name=".ScanActivity" /> + <activity android:name=".SendActivity" /> </application> <uses-permission android:name="android.permission.BLUETOOTH"/> diff --git a/app/src/main/java/com/github/antweb/donkey/FileTransfer.kt b/app/src/main/java/com/github/antweb/donkey/FileTransfer.kt index fe1f307f05bf8882b789a00b2f5f8ed0b6b92036..f2e6399e01d3ddc41536e0eac9db8a24e0ac410c 100644 --- a/app/src/main/java/com/github/antweb/donkey/FileTransfer.kt +++ b/app/src/main/java/com/github/antweb/donkey/FileTransfer.kt @@ -2,15 +2,22 @@ package com.github.antweb.donkey import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor import android.os.Environment +import android.util.Log import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream import java.nio.ByteBuffer +import java.util.* +import java.util.zip.CRC32 + +private const val TAG = "FileTransfer" class FileTransfer( private val gatt: BluetoothGatt, - private val dataCharacteristic: BluetoothGattCharacteristic, + private val tx: BluetoothGattCharacteristic, + private val rx: BluetoothGattCharacteristic, private val mtu: Int ) { private val filePath = "/Download/SEND.txt" @@ -18,6 +25,9 @@ class FileTransfer( private val iterator = openFile().iterator() private var offset: Int = 0 + private var lastCrc: Long = 0 + + private fun openFile(): ByteArray { // TODO: Don't listen to Android Docs, use stuff that is not deprecated val file = File( @@ -40,16 +50,15 @@ class FileTransfer( } fun sendNext() { + Log.d(TAG, "Sending next chunk: $offset") + if (!iterator.hasNext()) { return } - gatt?.beginReliableWrite() - - - val chunk = mutableListOf<Byte>() - for (i in 0 until (mtu - 5)) { +// for (i in 0 until (mtu - 5)) { + for (i in 0 until (10)) { chunk.add(iterator.next()) } @@ -61,12 +70,18 @@ class FileTransfer( val sendBuffer = ByteBuffer.allocate(chunk.size + 4) sendBuffer.putInt(header) sendBuffer.put(chunk.toByteArray()) + val bytes = sendBuffer.array() + val crc = CRC32() + crc.update(bytes) + lastCrc = crc.value - dataCharacteristic?.value = sendBuffer.array() - gatt?.writeCharacteristic(dataCharacteristic) +// gatt?.beginReliableWrite() + tx?.value = bytes + val status = gatt?.writeCharacteristic(tx) - // This will trigger the onCharacteristicWrite callback once the other side ACKs - // In there, we can call the actual gatt?.executeReliableWrite() + if (!status) { + Log.d(TAG, "Write status: $status") + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/antweb/donkey/MainActivity.kt b/app/src/main/java/com/github/antweb/donkey/MainActivity.kt index 2f79f33cb9831770297e55345f93c0cbf7cfc034..ff8611be17dd2fbfffe95d48a38f2dba0d053919 100644 --- a/app/src/main/java/com/github/antweb/donkey/MainActivity.kt +++ b/app/src/main/java/com/github/antweb/donkey/MainActivity.kt @@ -15,22 +15,6 @@ private const val TAG = "MainActivity" class MainActivity : AppCompatActivity() { - private val targetDeviceName = "card10" - private val mtu = 128 - private val serviceUuid = "00422342-2342-2342-2342-234223422342" - private val dataCharacteristicUuid = "01422342-2342-2342-2342-234223422342" - - private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { - val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothManager.adapter - } - - private var bluetoothGatt: BluetoothGatt? = null - private var fileService: BluetoothGattService? = null - private var dataCharacteristic: BluetoothGattCharacteristic? = null - private var mGatt: BluetoothGatt? = null - private var fileTransferService: FileTransfer? = null - private var mScanning: Boolean = false private var connected = false @@ -55,102 +39,4 @@ class MainActivity : AppCompatActivity() { startActivity(intent) } } - - private fun scanLeDevice() { - val gattCallback = object : BluetoothGattCallback() { - override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { - super.onServicesDiscovered(gatt, status) - - if (gatt == null) { - throw NullPointerException() - } - - for (service in gatt.services) { - Log.d(TAG, "Found service: ${service.uuid}") - - if (service.uuid.toString() == serviceUuid) { - fileService = service - } - - for (characteristic in service.characteristics) { - Log.d(TAG, "Characteristic: ${characteristic.uuid}") - - if (characteristic.uuid.toString() == dataCharacteristicUuid) { - dataCharacteristic = characteristic - } - } - } - - if (fileService == null || dataCharacteristic == null) { - Log.e(TAG, "Could not find file transfer service") - return - } - - gatt.requestMtu(mtu) - } - - override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { - super.onConnectionStateChange(gatt, status, newState) - - when (newState) { - BluetoothGatt.STATE_CONNECTED -> { - runOnUiThread { - tvConnection.text = "STATE_CONNECTED" - } - - mGatt = gatt - gatt?.discoverServices() - } - BluetoothGatt.STATE_DISCONNECTED -> tvConnection.text = "STATE_DISCONNECTED" - BluetoothGatt.STATE_CONNECTING -> tvConnection.text = "STATE_CONNECTING" - BluetoothGatt.STATE_DISCONNECTING -> tvConnection.text = "STATE_DISCONNECTING" - } - } - - override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { - Log.d(TAG, "MTU changed to: $mtu") - - runOnUiThread { - tvValue.text = "MTU: $mtu" - } - - val lData = dataCharacteristic - if (gatt != null && lData != null) { - fileTransferService = FileTransfer(gatt, lData, mtu) - fileTransferService?.sendFile() - } - } - - override fun onReliableWriteCompleted(gatt: BluetoothGatt?, status: Int) { - // Last chunk sent successfully. Send next chunk - fileTransferService?.sendNext() - } - - override fun onCharacteristicWrite( - gatt: BluetoothGatt?, - characteristic: BluetoothGattCharacteristic?, - status: Int - ) { - gatt?.executeReliableWrite() - } - } - - val leScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ -> - - if (device.name == targetDeviceName) { - if (!connected) { - connected = true - bluetoothGatt = device.connectGatt(this, true, gattCallback, BluetoothDevice.TRANSPORT_LE) - } - } - } - - // Stops scanning after a pre-defined period - Handler().postDelayed({ - mScanning = false - bluetoothAdapter?.stopLeScan(leScanCallback) - }, 5000) - mScanning = true - bluetoothAdapter?.startLeScan(leScanCallback) - } } diff --git a/app/src/main/java/com/github/antweb/donkey/ScanActivity.kt b/app/src/main/java/com/github/antweb/donkey/ScanActivity.kt index e24419af80064a80ea43acb7315cc3258e7ff676..988faf41e411701ad842156d06b8c0f51cbd309c 100644 --- a/app/src/main/java/com/github/antweb/donkey/ScanActivity.kt +++ b/app/src/main/java/com/github/antweb/donkey/ScanActivity.kt @@ -14,6 +14,11 @@ private const val TAG = "ScanActivity" class ScanActivity : AppCompatActivity() { + // HACK: Figure out how to transfer this later + companion object { + var selectedDevice: BluetoothDevice? = null + } + private lateinit var listView: ListView private lateinit var listAdapter: DeviceListAdapter @@ -32,6 +37,16 @@ class ScanActivity : AppCompatActivity() { listAdapter = DeviceListAdapter(applicationContext) listView.adapter = listAdapter + listView.setOnItemClickListener { adapterView, view, i, l -> + val item = adapterView.adapter.getItem(i) as? BluetoothDevice + + if (item != null) { + selectedDevice = item + val intent = Intent(this, SendActivity::class.java) + startActivity(intent) + } + } + checkPermissions() scan() } @@ -47,13 +62,6 @@ class ScanActivity : AppCompatActivity() { val foundDevices = mutableSetOf<BluetoothDevice>() val leScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ -> -// if (device.name == targetDeviceName) { -// if (!connected) { -// connected = true -// bluetoothGatt = device.connectGatt(this, true, gattCallback, BluetoothDevice.TRANSPORT_LE) -// } -// } - if (!foundDevices.contains(device)) { foundDevices.add(device) listAdapter.add(device) diff --git a/app/src/main/java/com/github/antweb/donkey/SendActivity.kt b/app/src/main/java/com/github/antweb/donkey/SendActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..64d259e912a2a632ed70a49d4d10ae2cbaa35adc --- /dev/null +++ b/app/src/main/java/com/github/antweb/donkey/SendActivity.kt @@ -0,0 +1,174 @@ +package com.github.antweb.donkey + +import android.bluetooth.* +import android.content.Context +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import java.lang.NullPointerException +import java.util.* + +private const val TAG = "SendActivity" + +class SendActivity : AppCompatActivity() { + private val mtu = 128 + private val serviceUuid = "00422342-2342-2342-2342-234223422342" + + private val centralTxCharacteristicUuid = UUID.fromString("01422342-2342-2342-2342-234223422342") +// private var centralTxCharacteristic: BluetoothGattCharacteristic? = null + + private val centralRxCharacteristicUuid = UUID.fromString("02422342-2342-2342-2342-234223422342") +// private var centralRxCharacteristic: BluetoothGattCharacteristic? = null + + private val clientConfigUuid = "00002902-0000-1000-8000-00805f9b34fb" + + + private var bluetoothGatt: BluetoothGatt? = null + private var fileService: BluetoothGattService? = null + + private var mGatt: BluetoothGatt? = null + private var fileTransferService: FileTransfer? = null + + private lateinit var tvConnection: TextView + private lateinit var tvValue: TextView + + private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { + val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothManager.adapter + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_send) + + tvValue = findViewById(R.id.text_value) + tvConnection = findViewById(R.id.text_connection_status) + tvConnection.text = "STATE_DISCONNECTED" + + val device = ScanActivity.selectedDevice + if (device != null) { + connect(device) + } else { + Log.e(TAG, "Device is NULL!") + } + } + + fun connect(device: BluetoothDevice) { + val gattCallback = object : BluetoothGattCallback() { + override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { + super.onServicesDiscovered(gatt, status) + + if (gatt == null) { + throw NullPointerException() + } + + for (service in gatt.services) { + Log.d(TAG, "Found service: ${service.uuid}") + + if (service.uuid.toString() == serviceUuid) { + fileService = service + } + +// for (characteristic in service.characteristics) { +// Log.d(TAG, "Characteristic: ${characteristic.uuid}") +// +// if (characteristic.uuid.toString() == centralTxCharacteristicUuid) { +// centralTxCharacteristic = characteristic +// } else if (characteristic.uuid.toString() == centralRxCharacteristicUuid) { +// centralRxCharacteristic = characteristic +// } +// } + } + + if (fileService == null) { +// if (fileService == null || centralTxCharacteristic == null) { + Log.e(TAG, "Could not find file transfer service") + return + } + + gatt.requestMtu(mtu) + } + + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + + when (newState) { + BluetoothGatt.STATE_CONNECTED -> { + runOnUiThread { + tvConnection.text = "STATE_CONNECTED" + } + + mGatt = gatt + gatt?.discoverServices() + } + BluetoothGatt.STATE_DISCONNECTED -> tvConnection.text = "STATE_DISCONNECTED" + BluetoothGatt.STATE_CONNECTING -> tvConnection.text = "STATE_CONNECTING" + BluetoothGatt.STATE_DISCONNECTING -> tvConnection.text = "STATE_DISCONNECTING" + } + } + + override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) { + super.onCharacteristicChanged(gatt, characteristic) + } + + override fun onCharacteristicRead( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + status: Int + ) { + super.onCharacteristicRead(gatt, characteristic, status) + } + + override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) { + Log.d(TAG, "MTU changed to: $mtu") + + runOnUiThread { + tvValue.text = "MTU: $mtu" + } + + val tx = fileService?.getCharacteristic(centralTxCharacteristicUuid) + val rx = fileService?.getCharacteristic(centralRxCharacteristicUuid) + + + if (gatt != null && tx != null && rx != null) { + val descriptor = rx.getDescriptor(UUID.fromString(clientConfigUuid)) + if (descriptor != null) { + gatt.setCharacteristicNotification(rx, true) + + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + val success = gatt.writeDescriptor(descriptor) + + if (!success) { + Log.e(TAG, "Descriptor write failed") + } + } else { + Log.e(TAG, "Failed to write descriptor") + } + + + fileTransferService = FileTransfer(gatt, tx, rx, mtu) + fileTransferService?.sendFile() + } + } + + override fun onReliableWriteCompleted(gatt: BluetoothGatt?, status: Int) { + // Last chunk sent successfully. Send next chunk +// fileTransferService?.sendNext() + } + + override fun onCharacteristicWrite( + gatt: BluetoothGatt?, + characteristic: BluetoothGattCharacteristic?, + status: Int + ) { +// gatt?.executeReliableWrite() + Thread.sleep(3000) + fileTransferService?.sendNext() + return + } + } + + bluetoothGatt = device.connectGatt(this, true, gattCallback, BluetoothDevice.TRANSPORT_LE) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_send.xml b/app/src/main/res/layout/activity_send.xml index fb3d8a258054c81b274252adffcf37b375659a36..085bff438a8ea858cb8ef0c78a0dbeb4f0d24b5d 100644 --- a/app/src/main/res/layout/activity_send.xml +++ b/app/src/main/res/layout/activity_send.xml @@ -4,4 +4,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/text_connection_status" + /> + + <TextView android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/text_value" + /> + </LinearLayout> \ No newline at end of file