bytes.cat

La wiki d'FP d'informàtica

Eines de l'usuari

Eines del lloc


android_bluetooth

Diferències

Ací es mostren les diferències entre la revisió seleccionada i la versió actual de la pàgina.

Enllaç a la visualització de la comparació

Següent revisió
Revisió prèvia
android_bluetooth [2026/01/10 16:31]
enric_mieza_sanchez creat
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?400 }} {{ android-bluetooth.jpg?400 }}
  
-En el cas del BLE podriem optar per 2 estratègies diferents:+Altres temes pendents d'abordar aquí: 
 +  * 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 que han estat aparellats al sistema operatiu.+La pràctica que s'inclou aquí està pensada per a rebre dades d'un dispositiu [[ESP32]]Podeu trobar [[esp32|aquí la pràctica de captura amb càmera i transmissió de dades per BLE]]. 
 +</WRAP>
  
 {{tag> #Dam #DamMp08 #DamMp08Uf2 #DamMp08Uf02 Android mobile kotlin bluetooth BLE BT }} {{tag> #Dam #DamMp08 #DamMp08Uf2 #DamMp08Uf02 Android mobile kotlin bluetooth BLE BT }}
  
 +\\
  
 ===== Llistar els dispositius emparellats ===== ===== Llistar els dispositius emparellats =====
Línia 16: Línia 25:
  
 <code kotlin> <code kotlin>
 +@SuppressLint("MissingPermission")
 fun updatePairedDevices() { fun updatePairedDevices() {
     // empty list     // empty list
Línia 21: Línia 31:
  
     // update list     // update list
-    val bluetoothAdapter BluetoothAdapter.getDefaultAdapter()+    val bluetoothManager getSystemService(BLUETOOTH_SERVICEas 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 103: Línia 114:
 } }
 </code> </code>
 +
 +\\
 +
 +===== 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'intercanvien dades entre dispositius Bluetooth Low Energy (BLE). És la base de tota comunicació en dispositius BLE com ara wearables, sensors mèdics, beacons, etc. GATT és essencial per a l'ecosistema IoT i dispositius wearables, permetent una comunicació estructurada i eficient entre dispositius.
 +
 +Conceptes claus:
 +  * **Profile**: Especificació estàndard (ex: Perfil de freqüència cardíaca)
 +  * **Service**: Col·lecció de característiques relacionades (ex: "Servei de freqüència cardíaca")
 +  * **Characteristic**: Conté una dada específica (ex: "Mesura de freqüència cardíaca")
 +  * **Descriptor**: Metadades sobre les característiques
 +
 +Us presento un codi per recepció de dades BLE encapsulat dins un ''Dialog'', per tal d'efectuar la recepció de les dades i tancar la comunicació, i tot dins d'un arxiu separat del codi de l'''Activity'' o ''Fragment''.
 +
 +El ''BLEconnDialog'' no gestiona els permisos, assumim que ja s'ha fet abans en el codi per accedir al llistat de dispositius emparellats o fent un //scan//.
 +
 +\\
 +
 +==== Layout Dialog ====
 +
 +El //layout// conté diversos ''TextView'' per a l'estat de la comunicació, una ''ProgressBar'' pel progrés de la transmissió i una ''ImageView'' per mostrar la imatge rebuda (que també s'enregistrarà al sistema d'arxius del mòbil).
 +
 +<file xml dialog_ble_conn.xml>
 +<?xml version="1.0" encoding="utf-8"?>
 +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 +    xmlns:app="http://schemas.android.com/apk/res-auto"
 +    xmlns:tools="http://schemas.android.com/tools"
 +    android:layout_width="match_parent"
 +    android:layout_height="wrap_content"
 +    android:orientation="vertical"
 +    android:padding="24dp">
 +
 +    <TextView
 +        android:id="@+id/tvDeviceName"
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        android:textSize="18sp"
 +        android:textStyle="bold"
 +        android:gravity="center"
 +        android:layout_marginBottom="16dp"/>
 +
 +    <TextView
 +        android:id="@+id/tvDeviceAddress"
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        android:textSize="14sp"
 +        android:gravity="center"
 +        android:layout_marginBottom="24dp"/>
 +
 +    <ProgressBar
 +        android:id="@+id/progressBar"
 +        style="?android:attr/progressBarStyleHorizontal"
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        android:indeterminate="true"
 +        android:layout_marginBottom="16dp"/>
 +
 +    <TextView
 +        android:id="@+id/tvStatus"
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        android:textSize="14sp"
 +        android:gravity="center"
 +        android:layout_marginBottom="24dp"/>
 +
 +    <LinearLayout
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        android:gravity="end"
 +        android:orientation="horizontal">
 +
 +        <Button
 +            android:id="@+id/btnCancel"
 +            android:layout_width="wrap_content"
 +            android:layout_height="wrap_content"
 +            android:layout_marginEnd="8dp"
 +            android:text="@android:string/cancel" />
 +
 +        <Button
 +            android:id="@+id/btnConnect"
 +            android:layout_width="wrap_content"
 +            android:layout_height="wrap_content"
 +            android:text="Connectar" />
 +
 +    </LinearLayout>
 +
 +    <ImageView
 +        android:id="@+id/imageView"
 +        android:layout_width="match_parent"
 +        android:layout_height="wrap_content"
 +        tools:srcCompat="@tools:sample/avatars" />
 +
 +</LinearLayout>
 +</file>
 +
 +\\
 +
 +==== BLEconnDialog ====
 +
 +<file kotlin BLEconnDialog.kt>
 +class BLEconnDialog(
 +    context: Context,
 +    private val device: BluetoothDevice,
 +    private val connectionCallback: BLEConnectionCallback
 +) : Dialog(context) {
 +
 +    interface BLEConnectionCallback {
 +        fun onConnectionSuccess(gatt: BluetoothGatt)
 +        fun onConnectionFailed(error: String)
 +        fun onConnectionCancelled()
 +        fun onReceivedImage(file: File)
 +    }
 +
 +    // Views
 +    private lateinit var tvDeviceName: TextView
 +    private lateinit var tvDeviceAddress: TextView
 +    private lateinit var tvStatus: TextView
 +    private lateinit var tvImage: ImageView
 +    private lateinit var progressBar: ProgressBar
 +    private lateinit var btnConnect: Button
 +    private lateinit var btnCancel: Button
 +
 +    // BLE
 +    // UUIDs
 +    private val SERVICE_UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
 +    private val CHARACTERISTIC_UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361b26a8")
 +    private var bluetoothGatt: BluetoothGatt? = null
 +    private val handler = Handler(Looper.getMainLooper())
 +    private var isConnecting = false
 +    private val CONNECTION_TIMEOUT = 10000L // 10 segons
 +    private val SCAN_PERIOD: Long = 10000
 +    private val RECEIVE_TIMEOUT: Long = 30000  // 30 segons 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("MissingPermission")
 +    private val connectionTimeoutRunnable = Runnable {
 +        if (isConnecting) {
 +            disconnect()
 +            connectionCallback.onConnectionFailed("Timeout de connexió")
 +            dismiss()
 +        }
 +    }
 +
 +    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
 +    override fun onCreate(savedInstanceState: Bundle?) {
 +        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 ?: "Dispositiu desconegut"
 +        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, CONNECTION_TIMEOUT)
 +
 +        // Connectar al dispositiu BLE
 +        // Nota: Necessites tenir el context de l'activitat o un context vàlid
 +        val context = context.applicationContext ?: context
 +        bluetoothGatt = device.connectGatt(context, false, gattCallback)
 +
 +        // En alguns dispositius, cal fer un connect explícit
 +        // bluetoothGatt?.connect()
 +    }
 +
 +    private fun updateUIForConnecting() {
 +        tvStatus.text = "Connectant..."
 +        btnConnect.isEnabled = false
 +        btnConnect.text = "Connectant..."
 +        progressBar.isIndeterminate = true
 +    }
 +
 +    private fun updateUIForConnected() {
 +        tvStatus.text = "Connectat"
 +        btnConnect.isEnabled = false
 +        btnConnect.text = "Connectat"
 +        progressBar.isIndeterminate = false
 +        progressBar.progress = 100
 +    }
 +
 +    private fun updateUIForDisconnected() {
 +        tvStatus.text = "Desconnectat"
 +        btnConnect.isEnabled = true
 +        btnConnect.text = "Connectar"
 +        progressBar.isIndeterminate = false
 +        progressBar.progress = 0
 +    }
 +
 +    private fun updateUIForReceived() {
 +        //tvStatus.text = "Desconnectat"
 +        btnConnect.isEnabled = true
 +        btnConnect.text = "Fet!"
 +        //progressBar.isIndeterminate = false
 +        //progressBar.progress = 0
 +    }
 +
 +
 +    @SuppressLint("MissingPermission")
 +    private val gattCallback = object : BluetoothGattCallback() {
 +        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
 +            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
 +                        //gatt.discoverServices()
 +                        // 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 -> "Timeout"
 +                                0x13 -> "Terminat per host local"
 +                                0x16 -> "Terminat per host remot"
 +                                0x3E -> "No connectat"
 +                                else -> "Error desconegut: $status"
 +                            }
 +
 +                            tvStatus.text = "Error: $errorMsg"
 +                            connectionCallback.onConnectionFailed(errorMsg)
 +                        }
 +                    }
 +                }
 +            }
 +        }
 +
 +        override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
 +            super.onMtuChanged(gatt, mtu, status)
 +            handler.post {
 +                Log.d("BLE", "MTU canviat a: $mtu")
 +                tvStatus.text = "MTU: $mtu"
 +            }
 +        }
 +
 +        @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
 +        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
 +            super.onServicesDiscovered(gatt, status)
 +
 +            if (status == BluetoothGatt.GATT_SUCCESS) {
 +                handler.post {
 +                    //binding.statusText.text = "Serveis descoberts"
 +                }
 +
 +                val service = gatt.getService(SERVICE_UUID)
 +                service?.let {
 +                    val photoCharacteristic = it.getCharacteristic(CHARACTERISTIC_UUID)
 +                    photoCharacteristic?.let { characteristic ->
 +                        // Habilitar notificacions
 +                        gatt.setCharacteristicNotification(characteristic, true)
 +
 +                        val descriptor = characteristic.getDescriptor(
 +                            UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
 +                        )
 +                        descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
 +                        gatt.writeDescriptor(descriptor)
 +
 +                        /*handler.post {
 +                            binding.statusText.text = "Llest per rebre fotos!"
 +                            binding.photoStatus.text = "Esperant foto..."
 +                            Toast.makeText(this@MainActivity,
 +                                "Connectat! Prem BOOT a l'ESP32",
 +                                Toast.LENGTH_SHORT).show()
 +                        }*/
 +                    } ?: run {
 +                        handler.post {
 +                            Log.d("BT","Error: Característica no trobada")
 +                            //binding.statusText.text = "Error: Característica no trobada"
 +                        }
 +                    }
 +                } ?: run {
 +                    handler.post {
 +                        Log.d("BT","Error: Servei no trobat")
 +                        //binding.statusText.text = "Error: Servei no trobat"
 +                    }
 +                }
 +            } else {
 +                handler.post {
 +                    Log.d("BT","Error descobrint serveis: $status")
 +                    //binding.statusText.text = "Error descobrint serveis: $status"
 +                }
 +            }
 +        }
 +
 +        override fun onCharacteristicChanged(gatt: BluetoothGatt,
 +                                             characteristic: BluetoothGattCharacteristic) {
 +            super.onCharacteristicChanged(gatt, characteristic)
 +
 +            if (characteristic.uuid == CHARACTERISTIC_UUID) {
 +                handleIncomingData(characteristic.value)
 +            }
 +        }
 +
 +        override fun onCharacteristicRead(gatt: BluetoothGatt,
 +                                          characteristic: BluetoothGattCharacteristic,
 +                                          status: Int) {
 +            super.onCharacteristicRead(gatt, characteristic, status)
 +            Log.d("BLE", "Característica llegida: ${characteristic.uuid}")
 +        }
 +
 +        override fun onCharacteristicWrite(gatt: BluetoothGatt,
 +                                           characteristic: BluetoothGattCharacteristic,
 +                                           status: Int) {
 +            super.onCharacteristicWrite(gatt, characteristic, status)
 +            Log.d("BLE", "Característica escrita: $status")
 +        }
 +
 +
 +
 +    }
 +
 +    @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
 +    private fun disconnect() {
 +        bluetoothGatt?.disconnect()
 +        bluetoothGatt?.close()
 +        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 = "BLEconnDialog"
 +    }
 +
 +
 +    // BLE data receive
 +    ///////////////////////
 +    private fun handleIncomingData(data: ByteArray) {
 +        handler.post {
 +            packetCount++
 +            lastPacketTime = System.currentTimeMillis()
 +
 +            Log.d("BLE", "Paquet $packetCount rebut: ${data.size} bytes")
 +            Log.d("BLE", "Primers bytes: ${data.take(4).joinToString("") { "%02X".format(it) }}")
 +
 +            // Verificar si és paquet de finalització
 +            if (data.size == 4 && data.contentEquals(byteArrayOf(0xFF.toByte(),
 +                    0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()
 +                ))) {
 +                if (isReceiving && receivedData.size() > 0) {
 +                    completePhotoTransfer()
 +                } else {
 +                    tvStatus.text = "Finalització rebuda sense dades"
 +                }
 +                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 = "Rebent foto ($totalSize bytes)..."
 +                    progressBar.max = totalSize
 +                    progressBar.progress = 0
 +
 +                    Log.d("BLE", "Mida anunciada: $totalSize bytes")
 +
 +                    // Iniciar timeout
 +                    startReceiveTimeout()
 +
 +                } catch (e: Exception) {
 +                    Log.e("BLE", "Error interpretant mida: ${e.message}")
 +                }
 +                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 =
 +                        "Rebent: $currentSize/$totalSize bytes ($percent%)"
 +
 +                    Log.d("BLE", "Progrés: $currentSize/$totalSize ($percent%)")
 +                }
 +
 +                // 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, RECEIVE_TIMEOUT)
 +    }
 +
 +    private fun resetReceiveTimeout() {
 +        handler.removeCallbacks(receiveTimeoutRunnable)
 +        handler.postDelayed(receiveTimeoutRunnable, RECEIVE_TIMEOUT)
 +    }
 +
 +    private val receiveTimeoutRunnable = Runnable {
 +        handler.post {
 +            if (isReceiving) {
 +                tvStatus.text = "⏰ Timeout! Transferència incompleta"
 +                Log.e("BLE", "Timeout en recepció de foto")
 +                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("FOTO",dataStr)
 +            // Guardar foto
 +            try {
 +                val decodedData = Base64.decode(dataStr,Base64.DEFAULT)
 +                savePhoto(decodedData)
 +                // Notificació
 +                //Toast.makeText(this, "Foto rebuda: $finalSize bytes",
 +                //    Toast.LENGTH_LONG).show()
 +                Log.v("BT","Foto rebuda: $finalSize bytes")
 +            } catch (e : Exception) {
 +                Log.v("ERROR","Error en descodificació base64")
 +                e.printStackTrace()
 +            }
 +
 +            // Reset
 +            resetPhotoTransfer()
 +        }
 +    }
 +
 +    private fun resetPhotoTransfer() {
 +        isReceiving = false
 +        totalSize = 0
 +        receivedData.reset()
 +        packetCount = 0
 +        handler.removeCallbacks(receiveTimeoutRunnable)
 +    }
 +
 +    private fun savePhoto(imageData: ByteArray) {
 +        try {
 +            val timestamp = System.currentTimeMillis()
 +            val filename = "ESP32_${timestamp}.jpg"
 +
 +            // Guardar al directori de Pictures
 +            val picturesDir = Environment.getExternalStoragePublicDirectory(
 +                Environment.DIRECTORY_PICTURES)
 +            val fri3dDir = File(picturesDir, "ESP32_Camera")
 +
 +            if (!fri3dDir.exists()) {
 +                fri3dDir.mkdirs()
 +            }
 +
 +            receivedFile = File(fri3dDir, filename)
 +            FileOutputStream(receivedFile).use { fos ->
 +                fos.write(imageData)
 +                fos.flush()
 +            }
 +
 +            Log.d("Photo", "Foto guardada: ${receivedFile.absolutePath}")
 +
 +            // 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("Photo", "Error guardant foto: ${e.message}")
 +            tvStatus.text = "❌ Error guardant foto"
 +        }
 +    }
 +
 +}
 +
 +</file>
 +
 +\\
 +
 +
 +==== Us del Dialog a Activity o Fragment ====
 +
 +Per utilitzar el ''BLEconnDialog'' afegirem la seva interfície a l'''Activity'':
 +
 +<code kotlin>
 +class MainActivity : AppCompatActivity(), BLEconnDialog.BLEConnectionCallback {
 +    // El codi de MainActivity aquí...
 +}
 +</code>
 +
 +I hi implementarem les //callback// necessàries.
 +
 +Quan volguem mostrar el ''BLEconnDialog'' cridarem ''showBLEDialog(device)'' amb el ''BluetoothDevice'' que haguem sel·leccionat prèviament (fent un //scan// o bé, en aquest cas, triant el device d'una llista de dispositius emparellats).
 +
 +<code kotlin>
 +// DIALOG : cridar aquesta funció per mostrar-lo
 +////////////////////////////////////////////////
 +private fun showBLEDialog(device: BluetoothDevice) {
 +    bleDialog = BLEconnDialog(this, device, this)
 +    bleDialog?.apply {
 +        setCancelable(false)
 +        setOnCancelListener {
 +            onConnectionCancelled()
 +        }
 +        show()
 +    }
 +}
 +
 +// DIALOG CALLBACKS
 +///////////////////////////////
 +override fun onConnectionSuccess(gatt: BluetoothGatt) {
 +    runOnUiThread {
 +        Toast.makeText(this, "Connectat amb èxit!", Toast.LENGTH_SHORT).show()
 +        // Aquí pots fer operacions amb el gatt connectat
 +        // Per exemple: llegir/escribre característiques
 +    }
 +}
 +
 +override fun onConnectionFailed(error: String) {
 +    runOnUiThread {
 +        Toast.makeText(this, "Error de connexió: $error", Toast.LENGTH_LONG).show()
 +    }
 +}
 +
 +override fun onConnectionCancelled() {
 +    runOnUiThread {
 +        Toast.makeText(this, "Connexió cancel·lada", Toast.LENGTH_SHORT).show()
 +    }
 +}
 +
 +override fun onReceivedImage(file: File) {
 +    runOnUiThread {
 +        val filename = file.name
 +        Toast.makeText(this, "Imatge rebuda: $filename", Toast.LENGTH_SHORT).show()
 +    }
 +}
 +
 +// Aquesta callback és de l'Activity
 +override fun onDestroy() {
 +    super.onDestroy()
 +    bleDialog?.dismiss()
 +}
 +</code>
 +
 +\\
 +
 +===== Transmissió de dades per BLE =====
 +
 +<WRAP tip>
 +La pràctica que s'inclou aquí està pensada per a rebre dades d'un dispositiu [[ESP32]]. Podeu trobar [[esp32|aquí la pràctica de captura amb càmera i transmissió de dades per BLE]].
 +</WRAP>
 +
 +\\
  
android_bluetooth.1768062703.txt.gz · Darrera modificació: 2026/01/10 16:31 per enric_mieza_sanchez