Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.
| Ambdós costats versió prèvia Revisió prèvia Següent revisió | Revisió prèvia | ||
|
android_bluetooth [2026/01/10 16:31] enric_mieza_sanchez [Android i Bluetooth] |
android_bluetooth [2026/01/25 19:33] (actual) enric_mieza_sanchez [Us del Dialog a Activity o Fragment] |
||
|---|---|---|---|
| Línia 1: | Línia 1: | ||
| ====== Android i Bluetooth ====== | ====== Android i Bluetooth ====== | ||
| + | |||
| + | El tema de Bluetooth és ample i complex. En aquest article abordarem el codi necessari per: | ||
| + | * Gestionar els **permisos per a comunicació Bluetooth**. | ||
| + | * Llistar i triar entre els **dispositius aparellats** al sistema operatiu. | ||
| + | * Utilitzar **GATT per a recepció de dades** entre dispositius BLE o Bluetooth Low Emission. | ||
| {{ android-bluetooth.jpg? | {{ android-bluetooth.jpg? | ||
| - | En el cas del BLE podriem optar per 2 estratègies diferents: | + | Altres temes pendents d' |
| + | * Escaneig de dispositius. | ||
| + | * GATT per a la transmissió de dades. | ||
| - | * Escanejar els dispositius des de la nostra app. | + | <WRAP tip> |
| - | * Llista i triar entre els dispositius | + | La pràctica |
| + | </ | ||
| {{tag> #Dam #DamMp08 #DamMp08Uf2 # | {{tag> #Dam #DamMp08 #DamMp08Uf2 # | ||
| Línia 17: | Línia 25: | ||
| <code kotlin> | <code kotlin> | ||
| + | @SuppressLint(" | ||
| fun updatePairedDevices() { | fun updatePairedDevices() { | ||
| // empty list | // empty list | ||
| Línia 22: | Línia 31: | ||
| // update list | // update list | ||
| - | val bluetoothAdapter | + | val bluetoothManager |
| + | val bluetoothAdapter = bluetoothManager.adapter | ||
| for( elem in bluetoothAdapter.bondedDevices.filter { device -> | for( elem in bluetoothAdapter.bondedDevices.filter { device -> | ||
| // Filtrar per dispositius BLE | // Filtrar per dispositius BLE | ||
| Línia 104: | Línia 114: | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | \\ | ||
| + | |||
| + | ===== Rebre fotos amb BLE GATT : BLEconnDialog ===== | ||
| + | |||
| + | Per la comunicació de dades (sèrie) es sol utilitzar el **Bluetooth GATT o Generic Attribute Profile**. GATT és un protocol de serveis i característiques que defineix com s' | ||
| + | |||
| + | Conceptes claus: | ||
| + | * **Profile**: | ||
| + | * **Service**: | ||
| + | * **Characteristic**: | ||
| + | * **Descriptor**: | ||
| + | |||
| + | Us presento un codi per recepció de dades BLE encapsulat dins un '' | ||
| + | |||
| + | El '' | ||
| + | |||
| + | \\ | ||
| + | |||
| + | ==== Layout Dialog ==== | ||
| + | |||
| + | El //layout// conté diversos '' | ||
| + | |||
| + | <file xml dialog_ble_conn.xml> | ||
| + | <?xml version=" | ||
| + | < | ||
| + | xmlns: | ||
| + | xmlns: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | style="? | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | <Button | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | <Button | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | |||
| + | </ | ||
| + | |||
| + | < | ||
| + | android: | ||
| + | android: | ||
| + | android: | ||
| + | tools: | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | |||
| + | \\ | ||
| + | |||
| + | ==== BLEconnDialog ==== | ||
| + | |||
| + | <file kotlin BLEconnDialog.kt> | ||
| + | class BLEconnDialog( | ||
| + | context: Context, | ||
| + | private val device: BluetoothDevice, | ||
| + | private val connectionCallback: | ||
| + | ) : Dialog(context) { | ||
| + | |||
| + | interface BLEConnectionCallback { | ||
| + | fun onConnectionSuccess(gatt: | ||
| + | fun onConnectionFailed(error: | ||
| + | fun onConnectionCancelled() | ||
| + | fun onReceivedImage(file: | ||
| + | } | ||
| + | |||
| + | // Views | ||
| + | private lateinit var tvDeviceName: | ||
| + | private lateinit var tvDeviceAddress: | ||
| + | private lateinit var tvStatus: TextView | ||
| + | private lateinit var tvImage: ImageView | ||
| + | private lateinit var progressBar: | ||
| + | private lateinit var btnConnect: Button | ||
| + | private lateinit var btnCancel: Button | ||
| + | |||
| + | // BLE | ||
| + | // UUIDs | ||
| + | private val SERVICE_UUID = UUID.fromString(" | ||
| + | private val CHARACTERISTIC_UUID = UUID.fromString(" | ||
| + | private var bluetoothGatt: | ||
| + | private val handler = Handler(Looper.getMainLooper()) | ||
| + | private var isConnecting = false | ||
| + | private val CONNECTION_TIMEOUT = 10000L // 10 segons | ||
| + | private val SCAN_PERIOD: | ||
| + | private val RECEIVE_TIMEOUT: | ||
| + | |||
| + | |||
| + | // Variables per foto | ||
| + | private val receivedData = ByteArrayOutputStream() | ||
| + | lateinit private var receivedFile : File | ||
| + | private var totalSize = 0 | ||
| + | private var isReceiving = false | ||
| + | private var received = false | ||
| + | private var lastPacketTime = 0L | ||
| + | private var packetCount = 0 | ||
| + | |||
| + | // Callback de timeout | ||
| + | @SuppressLint(" | ||
| + | private val connectionTimeoutRunnable = Runnable { | ||
| + | if (isConnecting) { | ||
| + | disconnect() | ||
| + | connectionCallback.onConnectionFailed(" | ||
| + | dismiss() | ||
| + | } | ||
| + | } | ||
| + | |||
| + | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) | ||
| + | override fun onCreate(savedInstanceState: | ||
| + | super.onCreate(savedInstanceState) | ||
| + | setContentView(R.layout.dialog_ble_conn) | ||
| + | |||
| + | // Inicialitzar views | ||
| + | tvDeviceName = findViewById(R.id.tvDeviceName) | ||
| + | tvDeviceAddress = findViewById(R.id.tvDeviceAddress) | ||
| + | tvStatus = findViewById(R.id.tvStatus) | ||
| + | tvImage = findViewById(R.id.imageView) | ||
| + | progressBar = findViewById(R.id.progressBar) | ||
| + | btnConnect = findViewById(R.id.btnConnect) | ||
| + | btnCancel = findViewById(R.id.btnCancel) | ||
| + | |||
| + | // Configurar dades del dispositiu | ||
| + | val deviceName = device.name ?: " | ||
| + | tvDeviceName.text = deviceName | ||
| + | tvDeviceAddress.text = device.address | ||
| + | |||
| + | // Configurar botons | ||
| + | btnConnect.setOnClickListener { | ||
| + | if( received ) { | ||
| + | connectionCallback.onReceivedImage(receivedFile) | ||
| + | disconnect() | ||
| + | dismiss() | ||
| + | } | ||
| + | if (!isConnecting) { | ||
| + | connectToDevice() | ||
| + | } | ||
| + | } | ||
| + | |||
| + | btnCancel.setOnClickListener { | ||
| + | cancelConnection() | ||
| + | } | ||
| + | |||
| + | // Connectar automàticament al mostrar el dialog | ||
| + | connectToDevice() | ||
| + | } | ||
| + | |||
| + | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) | ||
| + | private fun connectToDevice() { | ||
| + | if (isConnecting) return | ||
| + | |||
| + | isConnecting = true | ||
| + | updateUIForConnecting() | ||
| + | |||
| + | // Iniciar timeout | ||
| + | handler.postDelayed(connectionTimeoutRunnable, | ||
| + | |||
| + | // Connectar al dispositiu BLE | ||
| + | // Nota: Necessites tenir el context de l' | ||
| + | val context = context.applicationContext ?: context | ||
| + | bluetoothGatt = device.connectGatt(context, | ||
| + | |||
| + | // En alguns dispositius, | ||
| + | // bluetoothGatt? | ||
| + | } | ||
| + | |||
| + | private fun updateUIForConnecting() { | ||
| + | tvStatus.text = " | ||
| + | btnConnect.isEnabled = false | ||
| + | btnConnect.text = " | ||
| + | progressBar.isIndeterminate = true | ||
| + | } | ||
| + | |||
| + | private fun updateUIForConnected() { | ||
| + | tvStatus.text = " | ||
| + | btnConnect.isEnabled = false | ||
| + | btnConnect.text = " | ||
| + | progressBar.isIndeterminate = false | ||
| + | progressBar.progress = 100 | ||
| + | } | ||
| + | |||
| + | private fun updateUIForDisconnected() { | ||
| + | tvStatus.text = " | ||
| + | btnConnect.isEnabled = true | ||
| + | btnConnect.text = " | ||
| + | progressBar.isIndeterminate = false | ||
| + | progressBar.progress = 0 | ||
| + | } | ||
| + | |||
| + | private fun updateUIForReceived() { | ||
| + | // | ||
| + | btnConnect.isEnabled = true | ||
| + | btnConnect.text = " | ||
| + | // | ||
| + | // | ||
| + | } | ||
| + | |||
| + | |||
| + | @SuppressLint(" | ||
| + | private val gattCallback = object : BluetoothGattCallback() { | ||
| + | override fun onConnectionStateChange(gatt: | ||
| + | handler.post { | ||
| + | when (newState) { | ||
| + | BluetoothProfile.STATE_CONNECTED -> { | ||
| + | // Cancel·lar timeout | ||
| + | handler.removeCallbacks(connectionTimeoutRunnable) | ||
| + | isConnecting = false | ||
| + | |||
| + | // Actualitzar UI | ||
| + | updateUIForConnected() | ||
| + | |||
| + | // Sol·licitar MTU més gran | ||
| + | gatt.requestMtu(517) | ||
| + | |||
| + | // Descobrir serveis | ||
| + | // | ||
| + | // Descobrir serveis després d'un breu retard | ||
| + | handler.postDelayed({ | ||
| + | gatt.discoverServices() | ||
| + | }, 500) | ||
| + | } | ||
| + | |||
| + | BluetoothProfile.STATE_DISCONNECTED -> { | ||
| + | handler.removeCallbacks(connectionTimeoutRunnable) | ||
| + | isConnecting = false | ||
| + | |||
| + | if (status == BluetoothGatt.GATT_SUCCESS) { | ||
| + | updateUIForDisconnected() | ||
| + | } else { | ||
| + | // Error de connexió | ||
| + | val errorMsg = when (status) { | ||
| + | 0x08 -> " | ||
| + | 0x13 -> " | ||
| + | 0x16 -> " | ||
| + | 0x3E -> "No connectat" | ||
| + | else -> "Error desconegut: $status" | ||
| + | } | ||
| + | |||
| + | tvStatus.text = " | ||
| + | connectionCallback.onConnectionFailed(errorMsg) | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onMtuChanged(gatt: | ||
| + | super.onMtuChanged(gatt, | ||
| + | handler.post { | ||
| + | Log.d(" | ||
| + | tvStatus.text = "MTU: $mtu" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) | ||
| + | override fun onServicesDiscovered(gatt: | ||
| + | super.onServicesDiscovered(gatt, | ||
| + | |||
| + | if (status == BluetoothGatt.GATT_SUCCESS) { | ||
| + | handler.post { | ||
| + | // | ||
| + | } | ||
| + | |||
| + | val service = gatt.getService(SERVICE_UUID) | ||
| + | service? | ||
| + | val photoCharacteristic = it.getCharacteristic(CHARACTERISTIC_UUID) | ||
| + | photoCharacteristic? | ||
| + | // Habilitar notificacions | ||
| + | gatt.setCharacteristicNotification(characteristic, | ||
| + | |||
| + | val descriptor = characteristic.getDescriptor( | ||
| + | UUID.fromString(" | ||
| + | ) | ||
| + | descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE | ||
| + | gatt.writeDescriptor(descriptor) | ||
| + | |||
| + | / | ||
| + | binding.statusText.text = "Llest per rebre fotos!" | ||
| + | binding.photoStatus.text = " | ||
| + | Toast.makeText(this@MainActivity, | ||
| + | " | ||
| + | Toast.LENGTH_SHORT).show() | ||
| + | }*/ | ||
| + | } ?: run { | ||
| + | handler.post { | ||
| + | Log.d(" | ||
| + | // | ||
| + | } | ||
| + | } | ||
| + | } ?: run { | ||
| + | handler.post { | ||
| + | Log.d(" | ||
| + | // | ||
| + | } | ||
| + | } | ||
| + | } else { | ||
| + | handler.post { | ||
| + | Log.d(" | ||
| + | // | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onCharacteristicChanged(gatt: | ||
| + | | ||
| + | super.onCharacteristicChanged(gatt, | ||
| + | |||
| + | if (characteristic.uuid == CHARACTERISTIC_UUID) { | ||
| + | handleIncomingData(characteristic.value) | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onCharacteristicRead(gatt: | ||
| + | characteristic: | ||
| + | status: Int) { | ||
| + | super.onCharacteristicRead(gatt, | ||
| + | Log.d(" | ||
| + | } | ||
| + | |||
| + | override fun onCharacteristicWrite(gatt: | ||
| + | | ||
| + | | ||
| + | super.onCharacteristicWrite(gatt, | ||
| + | Log.d(" | ||
| + | } | ||
| + | |||
| + | |||
| + | |||
| + | } | ||
| + | |||
| + | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) | ||
| + | private fun disconnect() { | ||
| + | bluetoothGatt? | ||
| + | bluetoothGatt? | ||
| + | bluetoothGatt = null | ||
| + | isConnecting = false | ||
| + | } | ||
| + | |||
| + | @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) | ||
| + | private fun cancelConnection() { | ||
| + | handler.removeCallbacks(connectionTimeoutRunnable) | ||
| + | disconnect() | ||
| + | connectionCallback.onConnectionCancelled() | ||
| + | dismiss() | ||
| + | } | ||
| + | |||
| + | override fun dismiss() { | ||
| + | handler.removeCallbacks(connectionTimeoutRunnable) | ||
| + | super.dismiss() | ||
| + | } | ||
| + | |||
| + | override fun onDetachedFromWindow() { | ||
| + | handler.removeCallbacks(connectionTimeoutRunnable) | ||
| + | super.onDetachedFromWindow() | ||
| + | } | ||
| + | |||
| + | companion object { | ||
| + | const val TAG = " | ||
| + | } | ||
| + | |||
| + | |||
| + | // BLE data receive | ||
| + | /////////////////////// | ||
| + | private fun handleIncomingData(data: | ||
| + | handler.post { | ||
| + | packetCount++ | ||
| + | lastPacketTime = System.currentTimeMillis() | ||
| + | |||
| + | Log.d(" | ||
| + | Log.d(" | ||
| + | |||
| + | // Verificar si és paquet de finalització | ||
| + | if (data.size == 4 && data.contentEquals(byteArrayOf(0xFF.toByte(), | ||
| + | 0xFF.toByte(), | ||
| + | ))) { | ||
| + | if (isReceiving && receivedData.size() > 0) { | ||
| + | completePhotoTransfer() | ||
| + | } else { | ||
| + | tvStatus.text = " | ||
| + | } | ||
| + | return@post | ||
| + | } | ||
| + | |||
| + | // Primer paquet (pot ser la mida) | ||
| + | if (!isReceiving && data.size == 4) { | ||
| + | // Intentar interpretar com a mida de 32 bits | ||
| + | try { | ||
| + | totalSize = (data[0].toInt() and 0xFF) + | ||
| + | ((data[1].toInt() and 0xFF) shl 8) + | ||
| + | ((data[2].toInt() and 0xFF) shl 16) + | ||
| + | ((data[3].toInt() and 0xFF) shl 24) | ||
| + | |||
| + | isReceiving = true | ||
| + | receivedData.reset() | ||
| + | |||
| + | tvStatus.text = " | ||
| + | progressBar.max = totalSize | ||
| + | progressBar.progress = 0 | ||
| + | |||
| + | Log.d(" | ||
| + | |||
| + | // Iniciar timeout | ||
| + | startReceiveTimeout() | ||
| + | |||
| + | } catch (e: Exception) { | ||
| + | Log.e(" | ||
| + | } | ||
| + | return@post | ||
| + | } | ||
| + | |||
| + | // Si estem rebent, afegir dades | ||
| + | if (isReceiving) { | ||
| + | receivedData.write(data) | ||
| + | |||
| + | val currentSize = receivedData.size() | ||
| + | progressBar.progress = currentSize | ||
| + | |||
| + | // Actualitzar estat cada certs paquets | ||
| + | if (packetCount % 10 == 0 || currentSize == totalSize) { | ||
| + | val percent = if (totalSize > 0) | ||
| + | (currentSize * 100) / totalSize else 0 | ||
| + | |||
| + | tvStatus.text = | ||
| + | " | ||
| + | |||
| + | Log.d(" | ||
| + | } | ||
| + | |||
| + | // Reiniciar timeout amb cada paquet | ||
| + | resetReceiveTimeout() | ||
| + | |||
| + | // Si hem arribat a la mida esperada, completar | ||
| + | if (totalSize > 0 && currentSize >= totalSize) { | ||
| + | completePhotoTransfer() | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private fun startReceiveTimeout() { | ||
| + | handler.removeCallbacks(receiveTimeoutRunnable) | ||
| + | handler.postDelayed(receiveTimeoutRunnable, | ||
| + | } | ||
| + | |||
| + | private fun resetReceiveTimeout() { | ||
| + | handler.removeCallbacks(receiveTimeoutRunnable) | ||
| + | handler.postDelayed(receiveTimeoutRunnable, | ||
| + | } | ||
| + | |||
| + | private val receiveTimeoutRunnable = Runnable { | ||
| + | handler.post { | ||
| + | if (isReceiving) { | ||
| + | tvStatus.text = "⏰ Timeout! Transferència incompleta" | ||
| + | Log.e(" | ||
| + | resetPhotoTransfer() | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private fun completePhotoTransfer() { | ||
| + | val finalSize = receivedData.size() | ||
| + | |||
| + | handler.post { | ||
| + | tvStatus.text = "✅ Foto rebuda: $finalSize bytes" | ||
| + | progressBar.progress = finalSize | ||
| + | |||
| + | val dataStr = receivedData.toString().trim() | ||
| + | Log.v(" | ||
| + | // Guardar foto | ||
| + | try { | ||
| + | val decodedData = Base64.decode(dataStr, | ||
| + | savePhoto(decodedData) | ||
| + | // Notificació | ||
| + | // | ||
| + | // Toast.LENGTH_LONG).show() | ||
| + | Log.v(" | ||
| + | } catch (e : Exception) { | ||
| + | Log.v(" | ||
| + | e.printStackTrace() | ||
| + | } | ||
| + | |||
| + | // Reset | ||
| + | resetPhotoTransfer() | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private fun resetPhotoTransfer() { | ||
| + | isReceiving = false | ||
| + | totalSize = 0 | ||
| + | receivedData.reset() | ||
| + | packetCount = 0 | ||
| + | handler.removeCallbacks(receiveTimeoutRunnable) | ||
| + | } | ||
| + | |||
| + | private fun savePhoto(imageData: | ||
| + | try { | ||
| + | val timestamp = System.currentTimeMillis() | ||
| + | val filename = " | ||
| + | |||
| + | // Guardar al directori de Pictures | ||
| + | val picturesDir = Environment.getExternalStoragePublicDirectory( | ||
| + | Environment.DIRECTORY_PICTURES) | ||
| + | val fri3dDir = File(picturesDir, | ||
| + | |||
| + | if (!fri3dDir.exists()) { | ||
| + | fri3dDir.mkdirs() | ||
| + | } | ||
| + | |||
| + | receivedFile = File(fri3dDir, | ||
| + | FileOutputStream(receivedFile).use { fos -> | ||
| + | fos.write(imageData) | ||
| + | fos.flush() | ||
| + | } | ||
| + | |||
| + | Log.d(" | ||
| + | |||
| + | // preview image a l'app | ||
| + | tvImage.setImageURI(receivedFile.toUri()) | ||
| + | |||
| + | // Notificar galeria | ||
| + | val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) | ||
| + | mediaScanIntent.data = android.net.Uri.fromFile(receivedFile) | ||
| + | context.sendBroadcast(mediaScanIntent) | ||
| + | |||
| + | tvStatus.text = "💾 Guardat: $filename" | ||
| + | received = true | ||
| + | updateUIForReceived() | ||
| + | //dismiss() | ||
| + | |||
| + | } catch (e: Exception) { | ||
| + | Log.e(" | ||
| + | tvStatus.text = "❌ Error guardant foto" | ||
| + | } | ||
| + | } | ||
| + | |||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | \\ | ||
| + | |||
| + | |||
| + | ==== Us del Dialog a Activity o Fragment ==== | ||
| + | |||
| + | Per utilitzar el '' | ||
| + | |||
| + | <code kotlin> | ||
| + | class MainActivity : AppCompatActivity(), | ||
| + | // El codi de MainActivity aquí... | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | I hi implementarem les // | ||
| + | |||
| + | Quan volguem mostrar el '' | ||
| + | |||
| + | <code kotlin> | ||
| + | // DIALOG : cridar aquesta funció per mostrar-lo | ||
| + | //////////////////////////////////////////////// | ||
| + | private fun showBLEDialog(device: | ||
| + | bleDialog = BLEconnDialog(this, | ||
| + | bleDialog? | ||
| + | setCancelable(false) | ||
| + | setOnCancelListener { | ||
| + | onConnectionCancelled() | ||
| + | } | ||
| + | show() | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // DIALOG CALLBACKS | ||
| + | /////////////////////////////// | ||
| + | override fun onConnectionSuccess(gatt: | ||
| + | runOnUiThread { | ||
| + | Toast.makeText(this, | ||
| + | // Aquí pots fer operacions amb el gatt connectat | ||
| + | // Per exemple: llegir/ | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onConnectionFailed(error: | ||
| + | runOnUiThread { | ||
| + | Toast.makeText(this, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onConnectionCancelled() { | ||
| + | runOnUiThread { | ||
| + | Toast.makeText(this, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | override fun onReceivedImage(file: | ||
| + | runOnUiThread { | ||
| + | val filename = file.name | ||
| + | Toast.makeText(this, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Aquesta callback és de l' | ||
| + | override fun onDestroy() { | ||
| + | super.onDestroy() | ||
| + | bleDialog? | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | \\ | ||
| + | |||
| + | ===== Transmissió de dades per BLE ===== | ||
| + | |||
| + | <WRAP tip> | ||
| + | La pràctica que s' | ||
| + | </ | ||
| + | |||
| + | \\ | ||