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: git checkout skel-java

Pentru solutie Java: git checkout solution-java

Pentru varianta kotlin: git checkout skel-kotlin

Pentru solutie Java: git checkout solution-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);
    }

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
    }
}

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);
}

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);
    }

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();
    }

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();
    }

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);
            }
        });
    }

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
}

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();
    }