Configurarea unui server Bluetooth

Astazi vom implementa patru clase in aplicatia noastra de chat peste Bluetooth:

  • MainActivity - Activitatea principala unde se va gasi interfata grafica a aplicatiei de chat
  • ConnectThread - Responsabil pentru conectarea la un socket Bluetooth
  • AcceptThread - Serverul care asteapta conexiuni
  • ConnectedThread - Threadul pe care vor comunica cele 2 dispozitive Bluetooth
Click pentru a vedea interfata grafica a aplicatiei

Preview aplicație

0. Descarcarea scheletului

Vom porni de la urmatorul schelet de cod:

git clone https://github.com/eim-lab/Laborator07.git

Pentru varianta java, alegeti app-skel-java.

Pentru varianta Kotlin, alegeti app-skel-kotlin.

Pasul 1. Configurarea interfetei grafice:

În schelet avem deja definită o interfață grafică in XML care include:

  • O listă pentru afișarea mesajelor din chat.
  • Un câmp text pentru introducerea mesajelor.
  • Buton pentru listarea dispozitivelor Bluetooth împerecheate.
  • Buton pentru trimiterea mesajelor.

In continuare, vom defini o fuctie numita initViews, care inițializează toate componentele grafice definite în XML.

De notat faptul a vom folosi un ArrayAdapter pentru a lega o sursa de date de un obiect din interfata grafica.



private void initViews() {
    // TODO 1: Implement the method that initializes the views
    // Conectăm componentele din XML la codul Java
    ListView chatListView = findViewById(R.id.chatListView);
    messageEditText = findViewById(R.id.messageEditText);
    sendButton = findViewById(R.id.sendButton);
    listDevicesButton = findViewById(R.id.listDevicesButton);

    // Setăm un eveniment pentru butonul de listare a dispozitivelor
    listDevicesButton.setOnClickListener(v -> listPairedDevices());

     // Pregătim o listă pentru mesaje
    chatMessages = new ArrayList<>();

    // Creăm un adaptor care transformă lista noastră de mesaje în elemente vizuale
    chatArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, chatMessages);
    
    // Schimbarile din adaptor se vor vedea imediat in interfata grafica. In acest exemplue, chatListView de tip ListView va fi actualizata cand facem schimbari la chatArrayAdapter
    chatListView.setAdapter(chatArrayAdapter);
}



private fun initViews() {
    // TODO 1: Implement the method that initializes the views
    // Conectăm componentele din XML la codul Kotlin
    val chatListView: ListView = findViewById(R.id.chatListView)
    messageEditText = findViewById(R.id.messageEditText)
    sendButton = findViewById(R.id.sendButton)
    listDevicesButton = findViewById(R.id.listDevicesButton)

    // Setăm un eveniment pentru butonul de listare a dispozitivelor
    listDevicesButton.setOnClickListener { listPairedDevices() }

     // Pregătim o listă pentru mesaje
    chatMessages = ArrayList()

    // Creăm un adaptor care transformă lista noastră de mesaje în elemente vizuale
    chatArrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, chatMessages)
    
    // Schimbarile din adaptor se vor vedea imediat in interfata grafica. In acest exemplue, chatListView de tip ListView va fi actualizata cand facem schimbari la chatArrayAdapter
    chatListView.adapter = chatArrayAdapter
}

Pasul 2. Initializarea conexiunii Bluetooth

Vom implementa o metoda initBluetooth, care inițializează conexiunea Bluetooth și verifică dacă telefonul suportă Bluetooth. In aceasta functie vom lua o referinta la dispozitivul Bluetooth sub forma unui BluetoothAdapter.



private void initBluetooth() {
    // Vom lua o referinta la adaptorul de bluetooth
    // de pe telefon. Putem vedea acest adaptor ca o interfata
    // cu driver-ul de bluetooth.
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

    // Verificăm dacă Bluetooth este disponibil
    if (bluetoothAdapter == null) {
        Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show();
        finish(); // Închidem aplicația dacă Bluetooth nu este disponibil
    }
}



private fun initBluetooth() {
    // Vom lua o referinta la adaptorul de bluetooth
    // de pe telefon. Putem vedea acest adaptor ca o interfata
    // cu driver-ul de bluetooth.
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()

    // Verificăm dacă Bluetooth este disponibil
    if (bluetoothAdapter == null) {
        Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show()
        finish() // Închidem aplicația dacă Bluetooth nu este disponibil
    }
}

Pasul 3. Gestionarea permisiunilor Bluetooth:

De ce sunt necesare permisiuni? Pe Android, orice funcție care accesează hardware-ul telefonului (precum Bluetooth) necesită permisiuni explicite din partea utilizatorului.

Adaugă metoda checkPermissions:



private void checkPermissions() {
    List<String> permissions = new ArrayList<>();

    // Pentru Android 12 sau mai nou, folosim permisiuni specifice Bluetooth
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
        permissions.add(Manifest.permission.BLUETOOTH_SCAN);
    } else {
        // Pentru versiuni mai vechi, accesul la locație este necesar
        permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
    }

    // Cerem permisiunile utilizatorului
    ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), REQUEST_PERMISSIONS);
}



private fun checkPermissions() {
    val permissions = mutableListOf<String>()

    // Pentru Android 12 sau mai nou, folosim permisiuni specifice Bluetooth
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
        permissions.add(Manifest.permission.BLUETOOTH_SCAN)
    } else {
        // Pentru versiuni mai vechi, accesul la locație este necesar
        permissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
    }

    // Cerem permisiunile utilizatorului
    ActivityCompat.requestPermissions(this, permissions.toTypedArray(), REQUEST_PERMISSIONS)
}

Pasul 4. Activarea Bluetooth programatic

Uneori, utilizatorii pot avea Bluetooth dezactivat. Aplicația trebuie să-l activeze automat.

Adaugă următorul cod:



@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    // TODO 4: handle permission results
    if (requestCode == REQUEST_PERMISSIONS) {
        boolean permissionGranted = true;

        // Verificăm dacă toate permisiunile au fost acordate
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionGranted = false;
                break;
            }
        }

        if (!permissionGranted) {
            Toast.makeText(this, "Permissions required for Bluetooth operation.", Toast.LENGTH_LONG).show();
            finish();
        }

        // Activăm Bluetooth dacă nu este deja activat
        if (!bluetoothAdapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
    }
}

    // Handle Bluetooth enable result
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_ENABLE_BT && resultCode != RESULT_OK) {
        Toast.makeText(this, "Bluetooth must be enabled to continue.", Toast.LENGTH_LONG).show();
        finish();
    }
    super.onActivityResult(requestCode, resultCode, data);
}



override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    // TODO 4: handle permission results
    if (requestCode == REQUEST_PERMISSIONS) {
        var permissionGranted = true

        // Verificăm dacă toate permisiunile au fost acordate
        for (result in grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionGranted = false
                break
            }
        }

        if (!permissionGranted) {
            Toast.makeText(this, "Permissions required for Bluetooth operation.", Toast.LENGTH_LONG).show()
            finish()
        }

        // Activăm Bluetooth dacă nu este deja activat
        if (!bluetoothAdapter.isEnabled) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                return
            }
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
        }
    }
}

    // Handle Bluetooth enable result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_ENABLE_BT && resultCode != RESULT_OK) {
        Toast.makeText(this, "Bluetooth must be enabled to continue.", Toast.LENGTH_LONG).show()
        finish()
    }
    super.onActivityResult(requestCode, resultCode, data)
}

Pasul 5. Listarea dispozitivelor împerecheate

Pentru a permite comunicarea, utilizatorul trebuie să selecteze un dispozitiv împerecheat. Pentru a simplifica acest exercitiu, vom face pairing manual si doar vom afisa o lista cu dispozitivele imperecheate deja.



private void listPairedDevices() {
    // TODO 5: Implement the method that displays a dialog for selecting a paired device
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return;
    }

    // Obținem dispozitivele împerecheate
    Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
    List<String> deviceList = new ArrayList<>();
    final List<BluetoothDevice> devices = new ArrayList<>();

    // Le adaugam in lista
    if (!pairedDevices.isEmpty()) {
        for (BluetoothDevice device : pairedDevices) {
            deviceList.add(device.getName() + "\n" + device.getAddress());
            devices.add(device);
        }
    }

    // Afișăm dispozitivele într-un dialog
    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Select Device");

    ArrayAdapter<String> deviceArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, deviceList);

    // Tratam situatia in care un dispozitiv este apasat in cadrul dialogului
    builder.setAdapter(deviceArrayAdapter, (dialog, which) -> {
        selectedDevice = devices.get(which);

        // deschidem un thread de comunicare
        connectThread = new ConnectThread(this, bluetoothAdapter, selectedDevice, MY_UUID);
        connectThread.start();
    });

    builder.show();
}



private fun listPairedDevices() {
    // TODO 5: Implement the method that displays a dialog for selecting a paired device
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return
    }

    // Obținem dispozitivele împerecheate
    val pairedDevices: Set<BluetoothDevice> = bluetoothAdapter.bondedDevices
    val deviceList = mutableListOf<String>()
    val devices = mutableListOf<BluetoothDevice>()

    // Le adaugam in lista
    if (pairedDevices.isNotEmpty()) {
        for (device in pairedDevices) {
            deviceList.add(device.name + "\n" + device.address)
            devices.add(device)
        }
    }

    // Afișăm dispozitivele într-un dialog
    val builder = AlertDialog.Builder(this)
    builder.setTitle("Select Device")

    val deviceArrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, deviceList)

    // Tratam situatia in care un dispozitiv este apasat in cadrul dialogului
    builder.setAdapter(deviceArrayAdapter) { dialog, which ->
        selectedDevice = devices[which]

        // deschidem un thread de comunicare
        connectThread = ConnectThread(this, bluetoothAdapter, selectedDevice, MY_UUID)
        connectThread.start()
    }

    builder.show()
}

Pasul 6. Pornirea serverului pentru conexiuni Bluetooth

Serverul va asculta conexiuni de la alte dispozitive Bluetooth. Aceasta funcționalitate este implementată prin clasa AcceptThread, care gestionează un socket Bluetooth pentru a asculta conexiuni.

Ce face metoda startServer?

  • Creează un thread care inițiază un server socket Bluetooth.
  • Serverul rămâne activ până când o conexiune este acceptată sau este oprit manual.


private void startServer() {
    // TODO 6: Implement server socket to listen for incoming connections
    // Inițializăm AcceptThread, care va gestiona conexiunile primite
    acceptThread = new AcceptThread(this, bluetoothAdapter, MY_UUID);
    acceptThread.start();
}



private fun startServer() {
    // TODO 6: Implement server socket to listen for incoming connections
    // Inițializăm AcceptThread, care va gestiona conexiunile primite
    acceptThread = AcceptThread(this, bluetoothAdapter, MY_UUID)
    acceptThread.start()
}

Ce este AcceptThread? - AcceptThread este o clasă separată care face toată munca grea pentru ascultarea conexiunilor. Codul acesteia va fi prezentat in urmatoarea sectiune (Comunicarea prin Bluetooth)

Pasul 7. Trimiterea de mesaje prin Bluetooth:

Aceasta metodă permite trimiterea mesajelor către dispozitivul conectat. Folosim ConnectedThread pentru a scrie datele prin conexiunea Bluetooth. Metoda are responsabilitatea sa:

  • Preia textul din câmpul de introducere al utilizatorului.
  • Trimita textul folosind connectedThread.
  • Adauge mesajul în interfață.


private void sendMessage() {
    // TODO 7: Implement the method that sends a message to the connected device
    sendButton.setOnClickListener(v -> {
        // Preluăm mesajul introdus de utilizator
        String message = messageEditText.getText().toString();

        // Verificăm dacă mesajul nu este gol și conexiunea este activă
        if (!message.isEmpty() && connectedThread != null) {

            // Trimitem mesajul ca un array de bytes
            connectedThread.write(message.getBytes());

            // Golește câmpul de text
            messageEditText.setText("");

            // Adaugă mesajul trimis în interfață
            addChatMessage("Me: " + message);
        }
    });
}



private fun sendMessage() {
    // TODO 7: Implement the method that sends a message to the connected device
    sendButton.setOnClickListener { v ->
        // Preluăm mesajul introdus de utilizator
        val message = messageEditText.text.toString()

        // Verificăm dacă mesajul nu este gol și conexiunea este activă
        if (message.isNotEmpty() && connectedThread != null) {

            // Trimitem mesajul ca un array de bytes
            connectedThread.write(message.toByteArray())

            // Golește câmpul de text
            messageEditText.setText("")

            // Adaugă mesajul trimis în interfață
            addChatMessage("Me: $message")
        }
    }
}

Clasa ConnectedThread gestionează comunicarea efectivă între două dispozitive conectate. Se ocupă de: citirea mesajelor primite si trimiterea mesajelor. Detaliile de implementare vor fi prezentate in urmatoarea sectiune (Comunicarea prin Bluetooth).

Metoda addChatMessage este o metodă auxiliară pentru actualizarea listei de mesaje din interfață:



private void addChatMessage(String message) {
    chatMessages.add(message); // Adaugă mesajul în listă
    chatArrayAdapter.notifyDataSetChanged(); // Actualizează interfața
}



private fun addChatMessage(message: String) {
    chatMessages.add(message) // Adaugă mesajul în listă
    chatArrayAdapter.notifyDataSetChanged() // Actualizează interfața
}

Pasul 8: Gestionarea opririi aplicației

Este important să eliberăm resursele utilizate de Bluetooth (socket-uri și thread-uri) când aplicația se închide. Vom face acest lucru în metoda onDestroy



@Override
protected void onDestroy() {
    super.onDestroy();
    // TODO 8: cleanup threads
    // Închidem AcceptThread dacă rulează
    if (acceptThread != null) acceptThread.cancel();

    // Închidem ConnectThread dacă rulează
    if (connectThread != null) connectThread.cancel();

    // Închidem ConnectedThread dacă rulează
    if (connectedThread != null) connectedThread.cancel();
}



override fun onDestroy() {
    super.onDestroy()
    // TODO 8: cleanup threads
    // Închidem AcceptThread dacă rulează
    acceptThread?.cancel()

    // Închidem ConnectThread dacă rulează
    connectThread?.cancel()

    // Închidem ConnectedThread dacă rulează
    connectedThread?.cancel()
}