Clase auxiliare

Vom construi urmatoarele clase: ConnectThread, AcceptThread si ConnectedThread. Aceste clase gestionează conexiunile Bluetooth și comunicarea dintre dispozitive. Vom detalia fiecare clasă, explicând rolul, implementarea și cum funcționează în contextul aplicației.

AcceptThread

Scop:

Gestionează acceptarea conexiunilor Bluetooth. Funcționează ca un server care așteaptă conexiuni de la alte dispozitive.

AcceptThread așteaptă și acceptă conexiuni de la alte dispozitive Bluetooth. AcceptThread este un simplu Thread: private class AcceptThread extends Thread.

Constructorul creează un BluetoothServerSocket, utilizat pentru a asculta conexiunile. Verifică permisiunile pentru Bluetooth înainte de a iniția socket-ul.



public AcceptThread(MainActivity activity, BluetoothAdapter adapter, UUID uuid) {
    this.mainActivity = activity;

    BluetoothServerSocket tempSocket = null;
    try {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                Log.d("AcceptThread", "BLUETOOTH_CONNECT permission not granted.");
                return;
            }
        }
        tempSocket = adapter.listenUsingRfcommWithServiceRecord("BluetoothChatApp", uuid);
    } catch (IOException e) {
        Log.e("AcceptThread", "Error creating server socket: " + e.getMessage());
    }
    this.serverSocket = tempSocket;
}



constructor(activity: MainActivity, adapter: BluetoothAdapter, uuid: UUID) {
    this.mainActivity = activity

    var tempSocket: BluetoothServerSocket? = null
    try {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                Log.d("AcceptThread", "BLUETOOTH_CONNECT permission not granted.")
                return
            }
        }
        tempSocket = adapter.listenUsingRfcommWithServiceRecord("BluetoothChatApp", uuid)
    } catch (e: IOException) {
        Log.e("AcceptThread", "Error creating server socket: ${e.message}")
    }
    this.serverSocket = tempSocket
}

Metoda Run()

Rulează un loop care așteaptă conexiuni. Când o conexiune este acceptată, o gestionează prin metoda manageConnectedSocket.



@Override
public void run() {
    if (serverSocket == null) {
        Log.d(TAG, "ServerSocket is null, cannot accept connections.");
        return;
    }

    BluetoothSocket socket;
    while (!Thread.currentThread().isInterrupted()) {
        try {
            // Așteaptă o conexiune
            socket = serverSocket.accept();
            if (socket != null) {
                Log.d(TAG, "Connection accepted. Managing socket...");

                 // Predă conexiunea aplicației
                mainActivity.manageConnectedSocket(socket);
                closeServerSocket();
                break;
            }
        } catch (IOException e) {
            Log.e(TAG, "Error while accepting connection: " + e.getMessage());
            break;
        }
    }
    Log.d(TAG, "AcceptThread exiting.");
}



override fun run() {
    if (serverSocket == null) {
        Log.d(TAG, "ServerSocket is null, cannot accept connections.")
        return
    }

    var socket: BluetoothSocket
    while (!Thread.currentThread().isInterrupted) {
        try {
            // Așteaptă o conexiune
            socket = serverSocket.accept()
            if (socket != null) {
                Log.d(TAG, "Connection accepted. Managing socket...")

                 // Predă conexiunea aplicației
                mainActivity.manageConnectedSocket(socket)
                closeServerSocket()
                break
            }
        } catch (e: IOException) {
            Log.e(TAG, "Error while accepting connection: ${e.message}")
            break
        }
    }
    Log.d(TAG, "AcceptThread exiting.")
}

Metoda Cancel()

Închide serverSocket, oprind acceptarea de conexiuni noi.



public void cancel() {
    Log.d(TAG, "Canceling AcceptThread...");
    closeServerSocket();
}

private void closeServerSocket() {
    try {
        if (serverSocket != null) {
            serverSocket.close();
            Log.d(TAG, "ServerSocket closed successfully.");
        }
    } catch (IOException e) {
        Log.e(TAG, "Error closing ServerSocket: " + e.getMessage());
    }
}



fun cancel() {
    Log.d(TAG, "Canceling AcceptThread...")
    closeServerSocket()
}

private fun closeServerSocket() {
    try {
        if (serverSocket != null) {
            serverSocket.close()
            Log.d(TAG, "ServerSocket closed successfully.")
        }
    } catch (e: IOException) {
        Log.e(TAG, "Error closing ServerSocket: ${e.message}")
    }
}

Clasa ConnectThread

ConnectThread încearcă să stabilească o conexiune cu un dispozitiv Bluetooth specificat. Creează un canal de comunicație (BluetoothSocket) către un alt dispozitiv Bluetooth.



public ConnectThread(MainActivity activity, BluetoothAdapter adapter, BluetoothDevice device, UUID uuid) {
    this.mainActivity = activity;
    this.bluetoothAdapter = adapter;

    BluetoothSocket tmp = null;
    try {
        if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        tmp = device.createRfcommSocketToServiceRecord(uuid);
    } catch (IOException e) {
        Log.d("Connect->Constructor", e.toString());
    }
    socket = tmp;
}



constructor(activity: MainActivity, adapter: BluetoothAdapter, device: BluetoothDevice, uuid: UUID) {
    this.mainActivity = activity
    this.bluetoothAdapter = adapter

    var tmp: BluetoothSocket? = null
    try {
        if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return
        }
        tmp = device.createRfcommSocketToServiceRecord(uuid)
    } catch (e: IOException) {
        Log.d("Connect->Constructor", e.toString())
    }
    socket = tmp
}

Metoda run()

Încearcă să stabilească o conexiune. Dacă reușește, predă conexiunea aplicației.



public void run() {
    if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    if (socket == null) {
        Log.d("ConnectThread", "Socket is null, cannot connect.");
        return;
    }
    bluetoothAdapter.cancelDiscovery(); // Oprește căutarea altor dispozitive
    try {
        socket.connect(); // Se conectează la dispozitiv
        mainActivity.manageConnectedSocket(socket);
    } catch (IOException connectException) {
        mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connection failed.", Toast.LENGTH_SHORT).show());
        try {
            socket.close();
        } catch (IOException closeException) {
            Log.d("Connect->Run", closeException.toString());
        }
    }
}



override fun run() {
    if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
        return
    }
    if (socket == null) {
        Log.d("ConnectThread", "Socket is null, cannot connect.")
        return
    }
    bluetoothAdapter.cancelDiscovery() // Oprește căutarea altor dispozitive
    try {
        socket.connect() // Se conectează la dispozitiv
        mainActivity.manageConnectedSocket(socket)
    } catch (connectException: IOException) {
        mainActivity.runOnUiThread { Toast.makeText(mainActivity, "Connection failed.", Toast.LENGTH_SHORT).show() }
        try {
            socket.close()
        } catch (closeException: IOException) {
            Log.d("Connect->Run", closeException.toString())
        }
    }
}

Metoda cancel()

Închide conexiunea în caz de eroare sau la cererea utilizatorului.



public void cancel() {
    try {
        if (socket == null) {
            return;
        }
        socket.close();
    } catch (IOException e) {
        Log.d("Connect->Cancel", e.toString());
    }
}



fun cancel() {
    try {
        if (socket == null) {
            return
        }
        socket.close()
    } catch (e: IOException) {
        Log.d("Connect->Cancel", e.toString())
    }
}

ConnectedThread

ConnectedThread Gestionează trimiterea și primirea mesajelor după ce conexiunea este stabilită. O conexiunea este identificata printr-un socket bluetooth.

Constructorul primește un obiect BluetoothSocket și inițializează fluxurile de intrare și ieșire pentru comunicare. Configurează fluxurile de date (inputStream și outputStream) pentru a trimite și primi mesaje.



// BluetoothSocket este canalul de comuniatie stabilit cu un alt
// device bluetooth.
public ConnectedThread(MainActivity activity, BluetoothSocket socket) {
    this.mainActivity = activity;
    this.socket = socket;
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    try {
        tmpIn = socket.getInputStream();
    } catch (IOException e) {
        Log.d("Connected->Constructor", e.toString());
    }

    try {
        tmpOut = socket.getOutputStream();
    } catch (IOException e) {
        Log.d("Connected->Constructor", e.toString());
    }

    inputStream = tmpIn;
    outputStream = tmpOut;
}



// BluetoothSocket este canalul de comuniatie stabilit cu un alt
// device bluetooth.
constructor(activity: MainActivity, socket: BluetoothSocket) {
    this.mainActivity = activity
    this.socket = socket
    var tmpIn: InputStream? = null
    var tmpOut: OutputStream? = null

    try {
        tmpIn = socket.getInputStream()
    } catch (e: IOException) {
        Log.d("Connected->Constructor", e.toString())
    }

    try {
        tmpOut = socket.getOutputStream()
    } catch (e: IOException) {
        Log.d("Connected->Constructor", e.toString())
    }

    inputStream = tmpIn
    outputStream = tmpOut
}

Run()

Ascultă în mod continuu pentru mesaje primite și le afișează în interfață.



public void run() {
    byte[] buffer = new byte[1024];
    int bytes;

    if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return;
    }

    // obtine numele device-ului cu care comunicam
    String remoteDeviceName = socket.getRemoteDevice().getName();

    mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connected to " + remoteDeviceName, Toast.LENGTH_SHORT).show());

    while (true) {
        try {
            bytes = inputStream.read(buffer);  // Citește mesajul
            String incomingMessage = new String(buffer, 0, bytes);
            
            // Adaugam mesajul in threadul de UI al aplicatiei 
            mainActivity.runOnUiThread(() -> mainActivity.addChatMessage(remoteDeviceName + ": " + incomingMessage));
        } catch (IOException e) {
            mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Connection lost.", Toast.LENGTH_SHORT).show());
            break;
        }
    }
}



override fun run() {
    val buffer = ByteArray(1024)
    var bytes: Int

    if (ActivityCompat.checkSelfPermission(mainActivity, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return
    }

    // obtine numele device-ului cu care comunicam
    val remoteDeviceName = socket.getRemoteDevice().name

    mainActivity.runOnUiThread { Toast.makeText(mainActivity, "Connected to $remoteDeviceName", Toast.LENGTH_SHORT).show() }

    while (true) {
        try {
            bytes = inputStream.read(buffer)  // Citește mesajul
            val incomingMessage = String(buffer, 0, bytes)
            
            // Adaugam mesajul in threadul de UI al aplicatiei 
            mainActivity.runOnUiThread { mainActivity.addChatMessage("$remoteDeviceName: $incomingMessage") }
        } catch (e: IOException) {
            mainActivity.runOnUiThread { Toast.makeText(mainActivity, "Connection lost.", Toast.LENGTH_SHORT).show() }
            break
        }
    }
}

Write()

Trimite un mesaj prin fluxul de ieșire.



public void write(byte[] bytes) {
    try {
        outputStream.write(bytes);
    } catch (IOException e) {
        mainActivity.runOnUiThread(() -> Toast.makeText(mainActivity, "Failed to send message.", Toast.LENGTH_SHORT).show());
        Log.d("Connected->Write", e.toString());
    }
}



fun write(bytes: ByteArray) {
    try {
        outputStream.write(bytes)
    } catch (e: IOException) {
        mainActivity.runOnUiThread { Toast.makeText(mainActivity, "Failed to send message.", Toast.LENGTH_SHORT).show() }
        Log.d("Connected->Write", e.toString())
    }
}

Cancel()

Metoda cancel() închide conexiunea Bluetooth prin închiderea obiectului BluetoothSocket.



public void cancel() {
    try {
        if (socket == null) {
            Log.d("ConnectedThread", "Socket is null, cannot close.");
            return;
        }
        socket.close();
    } catch (IOException e) {
        Log.d("Connected->Cancel", e.toString());
    }
}



fun cancel() {
    try {
        if (socket == null) {
            Log.d("ConnectedThread", "Socket is null, cannot close.")
            return
        }
        socket.close()
    } catch (e: IOException) {
        Log.d("Connected->Cancel", e.toString())
    }
}

Rezumat

  • AcceptThread: Ascultă conexiunile Bluetooth primite.
  • ConnectThread: Inițiază conexiuni Bluetooth către alte dispozitive.
  • ConnectedThread: Gestionează comunicarea între dispozitive.

Aceste clase funcționează împreună pentru a crea o aplicație de chat complet funcțională. Asigură-te că gestionezi corect permisiunile și închizi conexiunile neutilizate!