vicent

学习积累

知识的积累需要时间,那就一步一步记录下来吧

Android Notes --- Bluetooth Development BT and BLE

Preface#

Before engaging in Bluetooth technology development, it is essential to understand the classification of Bluetooth modules:
Android Notes - Bluetooth Development BT and BLE
Currently, Bluetooth module development is divided into classic Bluetooth and low-energy Bluetooth development. Most popular IoT technologies on the market choose low-energy Bluetooth modules due to their high efficiency and low power consumption. The downside is that they only support small data transmissions, which began to be introduced after Android 4.3 (API 18). For larger data transmissions, such as audio and video, classic Bluetooth modules are required. It is crucial to distinguish between the types of Bluetooth modules before development, as the development steps for the two types differ. This article will cover the development steps for both Bluetooth modules.

General Development Steps#

The steps for Bluetooth module development are as follows:
Android Notes - Bluetooth Development BT and BLE

The development steps for both Bluetooth modules are roughly the same, with distinctions during Bluetooth connection and communication.

I. General API#

1. BluetoothAdapter#

The local Bluetooth adapter is used for basic Bluetooth operations, such as checking if Bluetooth is enabled, searching for Bluetooth devices, etc.

2. BluetoothDevice#

The Bluetooth device object contains some properties of Bluetooth devices, such as device name, MAC address, etc.

II. Classic Bluetooth (BT) API#

1. BluetoothSocket#

Represents the interface for Bluetooth sockets (similar to TCP Sockets; please refer to relevant computer networking content for the concept of sockets). Objects of this class serve as connection points for data transmission in applications.

2. BluetoothServerSocket#

Represents the server socket used to listen for future requests (similar to TCP ServerSocket). To enable two Bluetooth devices to connect, one device must use this class to open a server socket. When a remote Bluetooth device requests the server device, if the connection is accepted, BluetoothServerSocket will return a connected BluetoothSocket class object.

3. BluetoothClass#

Describes the main characteristics of Bluetooth devices. The BluetoothClass class object is a read-only set of Bluetooth device properties. Although this class object cannot reliably describe all contents of BluetoothProfile and all service information supported by the device, it still helps indicate the type of the device.

4. BluetoothProfile#

Represents Bluetooth specifications, which are standards for communication between Bluetooth devices.

III. Low Energy Bluetooth (BLE) API#

1. BluetoothGatt#

The Bluetooth Generic Attribute Profile defines the basic rules for BLE communication and is an implementation class of BluetoothProfile. Gatt stands for Generic Attribute Profile and is used for connecting devices, searching for services, and other operations.

2. BluetoothGattCallback#

Used to callback the results of operations after a Bluetooth device connects successfully; callbacks will only occur after a successful connection.

3. BluetoothGattService#

Services provided by Bluetooth devices, which are collections of Bluetooth device characteristics.

4. BluetoothGattCharacteristic#

Bluetooth device characteristics, which are the basic data units that make up GATT services.

5. BluetoothGattDescriptor#

Bluetooth device characteristic descriptors, which provide additional descriptions of characteristics.

1. Add Bluetooth Permissions#

(1) android.permission.BLUETOOTH#

To use Bluetooth functionality in your developed application, you must declare the Bluetooth permission "BLUETOOTH". This permission is required for Bluetooth communications, such as requesting connections, accepting connections, and exchanging data.

(2) android.permission.BLUETOOTH_ADMIN#

If your application needs to instantiate Bluetooth device searches or manipulate Bluetooth settings, you must declare the BLUETOOTH_ADMIN permission. Most applications require this permission to search for local Bluetooth devices. Other capabilities of this permission should not be used unless your application is a power management application that needs to modify Bluetooth settings.

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

It is important to note that after Android 6.0, Google requires location permissions for any access to hardware unique identifiers to better protect user data. Additionally, to search for nearby Bluetooth devices, the phone must provide location services; otherwise, calling Bluetooth scanning will yield no results.

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> 
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Add location permissions in AndroidManifest.xml. Location permissions are classified as Dangerous-level permissions, and don't forget to request them dynamically in your code.

2. Check Bluetooth Support#

		// Get Bluetooth adapter; if the adapter is null, the current phone does not support Bluetooth
		bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null) {
            Toast.makeText(this, "The current mobile device does not support Bluetooth", Toast.LENGTH_SHORT).show()
        }

3. Check if Bluetooth is Enabled#

(1) Determine if Bluetooth is enabled#

			// The mobile device supports Bluetooth; check if Bluetooth is enabled
            if (bluetoothAdapter!!.isEnabled) {
                Toast.makeText(this, "Mobile Bluetooth is enabled", Toast.LENGTH_SHORT).show()
                // You can perform Bluetooth search operations
                searchBtDevice()
            } else {
                // Bluetooth is not enabled; prompt to enable Bluetooth. It is recommended to use the second method to enable Bluetooth
                // First method: directly enable mobile Bluetooth without any prompt; generally not recommended
                //                bluetoothAdapter.enable();  //BLUETOOTH_ADMIN permission
                // Second method: friendly prompt to the user to enable Bluetooth
                val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
            }

(2) Friendly prompt to the user to enable Bluetooth#

                // Friendly prompt to the user to enable Bluetooth
                val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)

Android Notes - Bluetooth Development BT and BLE

(3) Listen for user actions to enable Bluetooth#

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_ENABLE_BT) {
            if (resultCode == Activity.RESULT_OK) {
                // Bluetooth is enabled
                // You can start searching for Bluetooth and get the Bluetooth list
                searchBtDevice()
            } else {
                // Bluetooth is not enabled; generally, the operation is to pop up a confirmation dialog to prompt the user to continue enabling Bluetooth
                DialogUtil.getInstance().showConfirmDialog(this, "Prompt", "Bluetooth is not enabled, would you like to enable Bluetooth?", "Enable",
                    "Cancel", object : DialogUtil.DialogConfirmListener<Any> {
                        override fun cancel() {
                        	// If the user chooses to cancel, you can close the current page or prompt that Bluetooth is not enabled
                            finish()
                        }

                        override fun confirm(data: Any?) {
                            // Continue to friendly prompt the user to enable Bluetooth
                            val enableBtIntent = Intent(ACTION_REQUEST_ENABLE)
                            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
                        }
                    })
            }
        }
    }

4. Scan for Bluetooth#

There are two methods for Bluetooth scanning. One is suitable for classic Bluetooth development mode, calling the Bluetooth adapter bluetoothAdapter.startDiscovery() to search for Bluetooth, and registering a Bluetooth broadcast receiver to obtain scanned Bluetooth information. This search method can scan both classic Bluetooth and low-energy Bluetooth. The other method is specifically for scanning low-energy Bluetooth, where only low-energy Bluetooth information will be scanned.

① Create a Bluetooth Broadcast Receiver#

public class BroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "BluetoothReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        LogUtil.logNormalMsg("----Bluetooth Broadcast Receiver----------action----------" + action);
        // Start searching
        if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) {
            if (onDeviceSearchListener != null) {
                onDeviceSearchListener.onDiscoveryStart();  // Start search callback
            }
            findConnectedBluetooth();
        } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
            // Search completed
            if (onDeviceSearchListener != null) {
                // Search completed callback
                onDeviceSearchListener.onDiscoveryStop();
            }

        } else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) {
            // 3.0 device found
            // Bluetooth device
            BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Signal strength
            int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);

            LogUtil.logNormalMsg("TAG", "Device found: " + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
            if (onDeviceSearchListener != null) {
                // 3.0 device found callback
                onDeviceSearchListener.onDeviceFound(bluetoothDevice, rssi);
            }
        } else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
            // Bluetooth disconnected
            // Bluetooth device
            BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            LogUtil.logNormalMsg("--------------Bluetooth Disconnected---------------");
            if (onDeviceSearchListener != null) {
                // Check if the current Bluetooth connection is disconnected
                if (bluetoothDevice.getAddress().equals(Content.bluetoothMac)){
                    onDeviceSearchListener.onDeviceDisconnect();
                }
            }
        } else if (TextUtils.equals(action, BluetoothDevice.ACTION_ACL_CONNECTED)) {
            // Bluetooth connected
            LogUtil.logNormalMsg("--------------Bluetooth Connected---------------");
            if (onDeviceSearchListener != null) {
                onDeviceSearchListener.onDeviceConnect();
            }
        } else if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
            // Classic Bluetooth pairing request can be used to intercept the pairing dialog and set the pairing code in the code
        }
    }
    /**
     * Bluetooth device search listener
     * 1. Start search
     * 2. Complete search
     * 3. Device found
     */
    public interface OnDeviceSearchListener {
        void onDiscoveryStart();   // Start search

        void onDiscoveryStop();    // Complete search

        void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi);  // Device found

        // Device disconnected
        void onDeviceDisconnect();

        // Device connected successfully
        void onDeviceConnect();
    }

    private OnDeviceSearchListener onDeviceSearchListener;

    public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) {
        this.onDeviceSearchListener = onDeviceSearchListener;
    }
}

② Dynamically Register Bluetooth Broadcast Receiver#

    /**
     * Register Bluetooth broadcast reception
     */
    private fun initBtBroadcast() {
        // Register broadcast reception
        btBroadcastReceiver = BtBroadcastReceiver()
        // The current Activity needs to implement the Bluetooth device search listener OnDeviceSearchListener 
        btBroadcastReceiver!!.setOnDeviceSearchListener(this)
        val intentFilter = IntentFilter()
        // Increase priority
        intentFilter.priority = 1001
        // Add Actions
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED) // Start scanning
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)// Scanning finished
        intentFilter.addAction(BluetoothDevice.ACTION_FOUND)// Device found
        intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)// Disconnected
        intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)// Connected
        intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)// Bluetooth pairing request
        registerReceiver(btBroadcastReceiver, intentFilter)

    }

③ Start Scanning for Bluetooth#

 /**
     * Start searching for Bluetooth
     */
    private fun searchBtDevice() {
        if (bluetoothAdapter!!.isDiscovering) {
            // Currently searching for devices... stop the current search
            bluetoothAdapter!!.cancelDiscovery()
        }
        // Start searching
        bluetoothAdapter!!.startDiscovery()
    }

After starting the scan, the Bluetooth broadcast receiver can listen for the Bluetooth devices scanned by the phone, and the Bluetooth device information can be obtained through the Bluetooth device search listener OnDeviceSearchListener method callback.

    override fun onDeviceFound(device: BluetoothDevice?, rssi: Int) {
        // This method will be called every time a Bluetooth device information is scanned; here you can get the Bluetooth device information and signal strength
    }

At this point, developers can display the list of scanned Bluetooth device information, and clicking on the list can initiate Bluetooth connection operations.
Android Notes - Bluetooth Development BT and BLE

Scanning is initiated using the startLeScan() method, and the scan results are directly processed in BluetoothAdapter.LeScanCallback or ScanCallback. Unlike classic Bluetooth mode, there is no need to register a broadcast to obtain scanned Bluetooth information. Note that the logic for calling the startScan method differs across versions; currently, most Android versions are above 5.0, so it is generally advisable to use the latter scanning method.

//1. Android 4.3 and above, below Android 5.0
mBluetoothAdapter.startLeScan(BluetoothAdapter.LeScanCallback LeScanCallback);
//2. Android 5.0 and above, scan results are processed in mScanCallback
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
mBluetoothLeScanner.startScan(ScanCallback mScanCallback);

Callback handling:

public abstract class ScanCallback {
    /**
     * Callback when a BLE advertisement has been found.
     *
     * @param callbackType Determines how this callback was triggered. Could be one of {@link
     * ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or
     * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST}
     * @param result A Bluetooth LE scan result.
     */
    public void onScanResult(int callbackType, ScanResult result) {
    	// Here you can handle the Bluetooth scan result
    	BluetoothDevice bluetoothDevice =  result.getDevice();
    }

    /**
     * Callback when batch results are delivered.
     *
     * @param results List of scan results that are previously scanned.
     */
    public void onBatchScanResults(List<ScanResult> results) {
    }

    /**
     * Callback when scan could not be started.
     *
     * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure.
     */
    public void onScanFailed(int errorCode) {
    }
}

If the development only involves low-energy Bluetooth technology, choosing the second method to scan for Bluetooth is sufficient, as it is simpler; otherwise, choose the first method.

5. Bluetooth Connection and Disconnection#

(1) Classic Bluetooth (BT)#

Connecting an app to a classic Bluetooth device is generally done through the createRfcommSocketToServiceRecord method of the BluetoothDevice object to obtain a BluetoothSocket, similar to socket programming. The connect() method is called to start the connection. Once connected, the input and output streams can be obtained through the getInputStream() and getOutputStream() methods of BluetoothSocket to communicate with Bluetooth. The createRfcommSocketToServiceRecord method requires a UUID parameter, which is generally known through the parameters of the Bluetooth device to be connected. If the UUID cannot be obtained, it can also be retrieved through reflection to get the BluetoothSocket object and then connect. It is important to note that Bluetooth connection is a time-consuming process and should not be executed on the main thread.

① Obtain BluetoothSocket using createRfcommSocketToServiceRecord#

		//1. Get BluetoothSocket
        try {
            // Establish a secure Bluetooth connection, which will pop up a pairing dialog
            mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
        } catch (IOException e) {
            e.printStackTrace();
            LogUtil.logNormalMsg(TAG, "Exception obtaining BluetoothSocket!" + e.getMessage());
        }

② Obtain BluetoothSocket via Reflection#

		//1. Get BluetoothSocket
        try {
            // Obtain Socket via reflection
            mSocket = (BluetoothSocket) mDevice.getClass().
                     getMethod("createRfcommSocket", new Class[]{int.class})
                     .invoke(mDevice, 1);
        } catch (IOException e) {
            e.printStackTrace();
            LogUtil.logNormalMsg(TAG, "Exception obtaining BluetoothSocket!" + e.getMessage());
        }

③ Connect to Bluetooth#

After obtaining the BluetoothSocket, you can connect to the Bluetooth device. After calling the connect() method, the mobile device will generally pop up a Bluetooth pairing dialog. Some Bluetooth devices may not show a dialog because the Bluetooth module does not have a pairing code set. This only applies to Bluetooth modules with pairing codes. After manually entering the pairing code, the connection is successful. Generally, the pairing codes for Bluetooth development devices are 0000 or 1234. When considering user experience, it is generally not advisable to let users input pairing codes themselves, as this operation seems cumbersome and unnecessary. During app development, it is generally considered to silently set the Bluetooth pairing code during the connection process, performing connection and pairing operations without user awareness. To perform this operation, developers need to intercept the Bluetooth pairing Action (BluetoothDevice.ACTION_PAIRING_REQUEST) and handle pairing in the code.

		mSocket.connect();

④ Silently Set Pairing Code#

		if (TextUtils.equals(action, BluetoothDevice.ACTION_PAIRING_REQUEST)) {
            BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Bluetooth pairing
            Log.i(TAG, "##### This is a pairing request ######");
            Bundle extras = intent.getExtras();
            Log.i(TAG, "-->" + extras.toString());
            Object device = extras.get("android.bluetooth.device.extra.DEVICE");
            Object pairKey = BaseData.bluetoothPassword;
            Log.i(TAG, "device-->" + String.valueOf(device));
            Log.i(TAG, "pairkey-->" + String.valueOf(pairKey));
            try {
                // Interrupt the pairing broadcast; if the broadcast is not terminated, a fleeting pairing dialog will appear.
                abortBroadcast();
                // Call setPin method for pairing...
                setPin(btDevice.getClass(), btDevice, String.valueOf(pairKey));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

Remember to add the Bluetooth pairing request Action when dynamically registering the Bluetooth broadcast receiver.

// Bluetooth pairing request Action
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)

Set the pairing code

	/**
     * Set Bluetooth pairing code
     * @param btClass btClass
     * @param btDevice BluetoothDevice
     * @param str str
     * @return boolean
     */
    public boolean setPin(Class btClass, BluetoothDevice btDevice, String str) {
        boolean flag = false;
        try {
            Class[] arrayOfClass = new Class[1];
            arrayOfClass[0] = byte[].class;
            Method removeBondMethod = btClass.getDeclaredMethod("setPin", arrayOfClass);
            Object[] arrayOfObject = new Object[1];
            arrayOfObject[0] = str.getBytes();
            flag = (Boolean) removeBondMethod.invoke(btDevice, arrayOfObject);
            LogUtil.logNormalMsg(TAG, "setPin result: " + flag);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (flag) {
            LogUtil.logNormalMsg("-------Successfully set Bluetooth pairing code");
        } else {
            LogUtil.logNormalMsg("-------Failed to set Bluetooth pairing code");
        }
        return flag;
    }

Here is my own Bluetooth connection code:

public class ConnectThread {
    private static final String TAG = "ConnectThread";
    private final BluetoothAdapter mBluetoothAdapter;
    private BluetoothSocket mmSocket;
    private final BluetoothDevice mDevice;

    public ConnectThread(BluetoothAdapter bluetoothAdapter, BluetoothDevice bluetoothDevice, String uuid) {
        this.mBluetoothAdapter = bluetoothAdapter;
        this.mDevice = bluetoothDevice;

        // Use a temporary variable to assign to mmSocket later
        // because mmSocket is static
        BluetoothSocket tmp = null;

        if (mmSocket != null) {
            LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null, releasing first");
            try {
                mmSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        LogUtil.logNormalMsg(TAG, "ConnectThread-->mmSocket != null has been released");

        //1. Get BluetoothSocket
        try {
            // Establish a secure Bluetooth connection, which will pop up a pairing dialog
            tmp = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));
        } catch (IOException e) {
            e.printStackTrace();
            LogUtil.logNormalMsg(TAG, "ConnectThread-->Exception obtaining BluetoothSocket!" + e.getMessage());
        }

        mmSocket = tmp;
        if (mmSocket != null) {
            LogUtil.logNormalMsg(TAG, "ConnectThread-->BluetoothSocket obtained");
        }

    }

    public void connectBluetooth() {
        connect();
    }

    private void connect() {
        // Before connecting, cancel device discovery; otherwise, it will significantly reduce the speed of connection attempts and increase the likelihood of connection failure
        if (mBluetoothAdapter == null) {
            LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mBluetoothAdapter == null");
            return;
        }
        // Cancel device discovery
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }

        if (mmSocket == null) {
            LogUtil.logNormalMsg(TAG, "ConnectThread:run-->mmSocket == null");
            return;
        }
        boolean firstFlag = false;
        //2. Connect to the device via socket
        try {
            LogUtil.logNormalMsg(TAG, "ConnectThread:run-->Attempting to connect...");
            if (onBluetoothConnectListener != null) {
                onBluetoothConnectListener.onStartConn();  // Start connection callback
            }

            mmSocket.connect();
            firstFlag = true;
        } catch (Exception e) {
            LogUtil.logNormalMsg(TAG, "ConnectThread:run-->First connection exception!" + e.getMessage());
            boolean flag = false;
            try {
                LogUtil.logNormalMsg(TAG, "ConnectThread:run-->Attempting second connection via reflection");
                mmSocket = (BluetoothSocket) mDevice.getClass().
                        getMethod("createRfcommSocket", new Class[]{int.class})
                        .invoke(mDevice, 1);
                mmSocket.connect();
                flag = true;
                Log.e(TAG, "mmSocket.isConnected():" + mmSocket.isConnected());

            } catch (Exception e2) {
                LogUtil.logNormalMsg("mmSocket connection failed: " + e.getMessage());
                e2.printStackTrace();
                LogUtil.logNormalMsg(TAG, "ConnectThread:run-->Connection exception 2!" + e2.getMessage());

                if (onBluetoothConnectListener != null) {
                    onBluetoothConnectListener.onConnFailure("Connection exception: " + e.getMessage());
                }
                // Release resources
                cancel();
            } finally {
                if (flag) {
                    if (onBluetoothConnectListener != null) {
                        // Connection successful callback
                        onBluetoothConnectListener.onConnSuccess(mmSocket);
                        LogUtil.logNormalMsg(TAG, "ConnectThread:run-->Connection successful");
                    }
                } else {
                    if (onBluetoothConnectListener != null) {
                        // Connection failed callback
                        onBluetoothConnectListener.onConnFailure("");
                    }
                }
            }
        } finally {
            if (firstFlag) {
                if (onBluetoothConnectListener != null) {
                    // Connection successful callback
                    onBluetoothConnectListener.onConnSuccess(mmSocket);
                    LogUtil.logNormalMsg(TAG, "ConnectThread:run-->Connection successful");
                }
            }
        }
    }



    /**
     * Release resources
     */
    public void cancel() {
        try {
            if (mmSocket != null && mmSocket.isConnected()) {
                LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected());
                mmSocket.close();
                mmSocket = null;
                return;
            }
            if (mmSocket != null) {
                mmSocket.close();
                mmSocket = null;
            }
            LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->Closed connected socket to release resources");
        } catch (IOException e) {
            LogUtil.logNormalMsg(TAG, "ConnectThread:cancel-->Exception closing connected socket to release resources!" + e.getMessage());
        }
    }

    private OnBluetoothConnectListener onBluetoothConnectListener;

    public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) {
        this.onBluetoothConnectListener = onBluetoothConnectListener;
    }

    // Connection status listener
    public interface OnBluetoothConnectListener {
        void onStartConn();  // Start connection

        void onConnSuccess(BluetoothSocket bluetoothSocket);  // Connection successful

        void onConnFailure(String errorMsg);  // Connection failed
    }

}

Once the Bluetooth connection is successful, communication can be established with the Bluetooth device through the input and output streams of BluetoothSocket. It is important to note that silently setting the Bluetooth pairing code generally requires negotiating a fixed pairing code with the Bluetooth device manufacturer; otherwise, constantly changing pairing codes may prevent the app from connecting to Bluetooth without any error messages.

⑤ Disconnect#

Disconnecting a classic Bluetooth connection essentially involves closing the BluetoothSocket and the input and output streams. It is important to close the input and output streams before closing the BluetoothSocket.

	try {
            if(mInStream != null){
                mInStream.close();  // Close input stream
            }
            if(mOutStream != null){
                mOutStream.close();  // Close output stream
            }
            if(mSocket != null){
                mSocket.close();   // Close socket
            }
            mInStream = null;
            mOutStream = null;
            mmSocket = null;
            LogUtil.logNormalMsg(TAG,"Successfully disconnected");
        } catch (Exception e) {
            e.printStackTrace();
            // If any part fails, forcibly close the socket connection
            mInStream = null;
            mOutStream = null;
            mmSocket = null;
            LogUtil.logNormalMsg(TAG, "Disconnection exception!" + e.getMessage());
        }

(2) Low Energy Bluetooth (BLE)#

The connection process for low-energy Bluetooth is slightly more complex:
Android Notes - Bluetooth Development BT and BLE

Before Connecting#

Before connecting to Bluetooth, it is necessary to understand GATT to deepen our understanding of low-energy Bluetooth development technology.

GATT Hierarchy#

Android Notes - Bluetooth Development BT and BLE
The top level of this hierarchy is a profile. A profile consists of one or more services required to meet a use case. Services consist of characteristics or references to other services. Each characteristic contains a value and may contain optional information about that value. Services, characteristics, and the components of characteristics (i.e., values and descriptors) contain profile data and are stored in attributes on the server.

Roles of Each Member#

  1. Profile

The profile specification defines the architecture for exchanging profile data. This architecture defines the basic elements used by the profile, such as services and characteristics. The highest level of this hierarchy is the profile. A profile consists of one or more services required to implement a use case. Services consist of characteristics or references to other services. Each characteristic includes a value and may include optional information about that value. Services, characteristics, and the components of characteristics (i.e., characteristic values and characteristic descriptors) constitute the profile data and are all stored in the server's attributes. In simple terms, this is a protocol specification; all Bluetooth compatibility relies on this specification, and using a standard Profile means you don't need to focus too much on it.

  1. Service

Services manage different data processing, such as battery level and heart rate; they do not participate in specific data exchanges but serve functional partitioning roles. Each service has a UUID for differentiation.

  1. Characteristic

Characteristics are one of the members that directly participate in data exchange. Characteristics also have a UUID for identification; the characteristic can be accessed via UUID, and modifying the value within the characteristic achieves communication. This communication mode is similar to the observer pattern; when the value changes, it notifies observers listening to this characteristic, informing them that data has changed, and the observer retrieves the data, completing a communication cycle. The specific implementation will be detailed later.

  1. Descriptor

Descriptors provide descriptions of characteristic values and are used to define the defined properties of characteristic values. For example, descriptors can specify readable descriptions, acceptable ranges for characteristic values, or specific measurement units for characteristic values. This is not used very often; if the central device does not receive data, check if the descriptor is not set to notify.

  1. Advertising

Advertising is a means for peripheral devices to make themselves discoverable; only by broadcasting can BLE be scanned. Once the device is connected, the broadcasting will stop.

Low-energy Bluetooth connects and manages through BluetoothGatt. The BluetoothGatt object can be obtained via the connectGatt() method of BluetoothDevice, which requires three parameters: context, autoConnect (whether to automatically connect), and callback (connection callback). The key is the third parameter, BluetoothGattCallback, as developers need to discover services and configure communication pipelines in the BluetoothGattCallback callback, while also listening for messages returned by the lower machine (Bluetooth module). It is important to note that obtaining services, characteristics, and characteristic value descriptors in GATT requires specifying UUIDs, which are generally obtained from the Bluetooth module manufacturer. UUIDs can also be discovered through code by iterating through all UUIDs to find the correct one, which will be discussed later.

connectGatt() method source code:

	/**
     * Connect to GATT Server hosted by this device. Caller acts as GATT client.
     * The callback is used to deliver results to Caller, such as connection status as well
     * as any further GATT client operations.
     * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
     * GATT client operations.
     *
     * @param callback GATT callback handler that will receive asynchronous callbacks.
     * @param autoConnect Whether to directly connect to the remote device (false) or to
     * automatically connect as soon as the remote device becomes available (true).
     * @throws IllegalArgumentException if callback is null
     */
    public BluetoothGatt connectGatt(Context context, boolean autoConnect,
            BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
    }

① Set Custom Callback#

/**
     * Low-energy Bluetooth connection callback
     * Receive Bluetooth data
     */
    inner class MyBluetoothGattCallback : BluetoothGattCallback() {
        override fun onConnectionStateChange(
            gatt: BluetoothGatt,
            status: Int,
            newState: Int
        ) {
            super.onConnectionStateChange(gatt, status, newState)
            Content.isConnectBluetoothNow = false
            // Connection successful; start discovering services
            when {
                newState == BluetoothAdapter.STATE_CONNECTED -> {
                	// At this point, gatt has successfully connected; you can call to discover gatt services
                    mGatt?.discoverServices()
                }
                newState == BluetoothAdapter.STATE_DISCONNECTED -> {
                    // Connection disconnected
                }
                status == 133 -> {
                    // Connection failed
                }
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            super.onServicesDiscovered(gatt, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                // At this point, all gatt services have been discovered; you can access them via gatt.services
				// If we know all UUIDs related to gatt, we can start configuring communication pipelines
				// Get the characteristic for writing
				writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
				// Get the characteristic for notification listening, equivalent to read listening; after enabling listening, you can listen to messages from the lower machine in real-time
				notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
				// Set to enable listening to real-time messages from the lower machine
				//1. Set characteristic notification
            	mGatt?.setCharacteristicNotification(notifyCharact , true)
            	//2. Get descriptor
            	val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
            	descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            	//3. Set descriptor; the result of this setting will be callbacked in onDescriptorWrite method 
            	mGatt?.writeDescriptor(descriptor)
            	// At this point, low-energy Bluetooth connection and configuration are complete
            }
        }

        override fun onCharacteristicRead(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {
            super.onCharacteristicRead(gatt, characteristic, status)
        }

        override fun onCharacteristicWrite(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic,
            status: Int
        ) {
            super.onCharacteristicWrite(gatt, characteristic, status)
            val value = characteristic.value
            val data = Util.bytesToAscii(value)
            // This method will be callbacked after successfully sending data
            LogUtil.logNormalMsg("onCharacteristicWrite", "Sent successfully: $data")
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic
        ) {
            super.onCharacteristicChanged(gatt, characteristic)
            // value is the data sent by the device; parse it according to the data protocol
            // Process received data
            handBluetoothData(characteristic.value)
        }

        override fun onDescriptorRead(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            super.onDescriptorRead(gatt, descriptor, status)
        }

        override fun onDescriptorWrite(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            super.onDescriptorWrite(gatt, descriptor, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                LogUtil.logNormalMsg("LockBluetoothService", "Descriptor set successfully, can send data now")
                // Some Bluetooth modules need to set mtu after successful connection; configure as needed
                mGatt?.requestMtu(103)
            }
        }
    }

② Start Bluetooth Connection#

After starting the Bluetooth connection, the BluetoothGatt object can be obtained via the connectGatt method, and the connection result can be processed in the previously defined BluetoothGattCallback.

mGatt = bluetoothDevice1!!.connectGatt(appContext, false, bluetoothGattCallback)

③ Discover GATT Services#

This operation must be performed after a successful connection; services must be discovered before configuring Bluetooth communication.

mGatt?.discoverServices()	

④ Configure Communication#

Obtain the BluetoothGattService service and the BluetoothGattCharacteristic characteristic for reading and writing, and configure Bluetooth communication.

				// Get the characteristic for writing
				writeCharact = this.bluetoothGattService?.getCharacteristic(writeUUID)
				// Get the characteristic for notification listening, equivalent to read listening; after enabling listening, you can listen to messages from the lower machine in real-time
				notifyCharact = this.bluetoothGattService?.getCharacteristic(notifyUUID)
				// Set to enable listening to real-time messages from the lower machine
				//1. Set characteristic notification
            	mGatt?.setCharacteristicNotification(notifyCharact , true)
            	//2. Get descriptor
            	val descriptor: BluetoothGattDescriptor = notifyCharact ?.getDescriptor(descriptorUUID)!!
            	descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            	//3. Set descriptor; the result of this setting will be callbacked in onDescriptorWrite method 
            	mGatt?.writeDescriptor(descriptor)
            	// At this point, low-energy Bluetooth connection and configuration are complete; communication can begin

Generally, Bluetooth development manufacturers will provide the relevant UUIDs. If the UUIDs of the Bluetooth module are unknown, they can be discovered by iterating through all services of GATT until the correct UUID is found.

    /**
     * Find low-energy Bluetooth UUID
     */
    private fun findUUID(gatt: BluetoothGatt) {
        LogUtil.logNormalMsg("gatt.getServices().size():" + gatt.services.size)
        if (gatt.services != null && gatt.services.size > 0) {
        	// Iterate through all services
            for (i in gatt.services.indices) {
                val service = gatt.services[i]
                LogUtil.logNormalMsg("---------------------------------------------")
               	// Output the current service's UUID
                LogUtil.logNormalMsg("service.getUuid=" + service.uuid)
                // Check if the current service has characteristics
                if (service.characteristics != null && service.characteristics.size > 0) {					// Iterate through all characteristics of the current service
                    for (j in service.characteristics.indices) {
                        val characteristic =
                            service.characteristics[j]
                        // Output the current characteristic's UUID
                        LogUtil.logNormalMsg("characteristic.getUuid=" + characteristic.uuid)
                        // Get characteristic properties
                        val charaProp = characteristic.properties
                        // Check if reading is supported
                        if (charaProp or BluetoothGattCharacteristic.PROPERTY_READ > 0) {
                            LogUtil.logNormalMsg("-------type:PROPERTY_READ")
                        }
                        // Check if writing is supported
                        if (charaProp or BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
                            LogUtil.logNormalMsg("-------type:PROPERTY_WRITE")
                        }
                        // Check if listening is supported
                        if (charaProp or BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
                            LogUtil.logNormalMsg("-------type:PROPERTY_NOTIFY")
                        }
                        if (characteristic.descriptors != null && characteristic.descriptors.size > 0) {
                            for (descriptor in characteristic.descriptors) {
                                LogUtil.logNormalMsg("descriptor.getUuid()=" + descriptor.uuid)
                            }
                        }
                    }
                }
            }
        }
    }

It is important to find comprehensive services; some services may not have characteristics, some characteristics may not support read/write, and some characteristics may not have descriptors, making it impossible to enable read listening.
Android Notes - Bluetooth Development BT and BLE

⑤ Disconnect#

	mGatt?.let {
            // Disconnect BluetoothGatt connection
            it.disconnect()
            // Close BluetoothGatt
            it.close()
        }

6. Communication#

(1) Classic Bluetooth Communication#

Based on the connection logic mentioned in section 5, BluetoothSocket can be obtained, similar to socket programming. Data communication with the Bluetooth module can be conducted through the input and output streams of BluetoothSocket. It is recommended to perform communication operations in a separate thread, continuously monitoring whether there is data available to read in the InputStream.

① Obtain Input and Output Streams#

		// Get InputStream and OutputStream
        try {
            mInStream = socket.getInputStream();
            mOutStream = socket.getOutputStream();

        } catch (IOException e) {
        	e.printStackTrace();
            LogUtil.logNormalMsg(TAG,"Exception obtaining InputStream and OutputStream!");
        }

② Send Data#

	// Send data
    public boolean write(byte[] bytes){
        try {
            if(mOutStream == null){
                LogUtil.logNormalMsg(TAG, "mmOutStream == null");
                return false;
            }
            // Send data
            mOutStream.write(bytes);
            mOutStream.flush();
            Log.d(TAG, "Write successful: "+ bytes2HexString(bytes, bytes.length));
            return true;

        } catch (IOException e) {
            LogUtil.logNormalMsg("Error sending data: "+e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

③ Read Data#

	// Thread's run method
 	@Override
    public void run(){
        // Maximum buffer size to store stream
        byte[] buffer = new byte[1024 * 2];  // Buffer store for the stream
        // Number of bytes read from the stream's read() method
        int bytes = 0;  // Bytes returned from read()
        // Continuously listen to the input stream until an exception occurs
        while(!isStop){
            try {
                if(mInStream == null){
                    LogUtil.logNormalMsg(TAG,"ConnectedThread:run-->Input stream mmInStream == null");
                    break;
                }
                // First check if there is data; read only if there is data
                if(mInStream.available() != 0){
                    // Read a certain number of bytes from (mmInStream) input stream (read content) and store them in the buffer array; bytes is the actual number of bytes read
                    bytes = mInStream.read(buffer);
                    LogUtil.logNormalMsg(TAG,"Length of data read: "+bytes);
                    // Store the actual data content read
                    byte[] b = Arrays.copyOf(buffer,bytes);
                    // Process Bluetooth data
                    handBluetoothData(b);
                }
                Thread.sleep(150);
            } catch (Exception e) {
                LogUtil.logNormalMsg(TAG,"Exception receiving message!" + e.getMessage());
                // Close stream and socket
                boolean isClose = cancel();
                if(isClose){
                    LogUtil.logNormalMsg(TAG,"Message reception exception, successfully disconnected!");
                }
                break;
            }
        }
        // Close stream and socket
        boolean isClose = cancel();
        if(isClose){
            Log.d(TAG,"Message reception ended, disconnected!");
        }
    }

(2) Low Energy Bluetooth Communication#

Low-energy Bluetooth communication is simpler; data can be sent directly through the characteristic being written, while data can be read in the onCharacteristicChanged callback method of BluetoothGattCallback, which listens for data sent by the Bluetooth device.

① Send Data#

	fun sendMsgToBluetooth(msg: ByteArray): Boolean {
        if (writeCharact != null) {
        	// Set the value of the characteristic
            writeCharact?.value = msg
            // Set the write type of the characteristic; choose the type based on needs; detailed reading of the source code is recommended
            writeCharact?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
            // Write data via gatt
            return mGatt!!.writeCharacteristic(writeCharact)
        }
        return false
    }

② Read Data#

 		override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic
        ) {
            super.onCharacteristicChanged(gatt, characteristic)
            // Process received data
            handBluetoothData(characteristic.value)
        }

7. Supplement#

(1) During multiple debugging sessions, it was found that in classic Bluetooth mode development, there are two situations where Bluetooth devices may not be scanned:

  1. The app disconnects the Bluetooth connection and then scans for Bluetooth again or restarts the app to scan again;
  2. When the app is started, the Bluetooth device is already paired and connected to the mobile app;
    In such cases, do not panic; simply find the Bluetooth device connected to the mobile and add it to the scanned Bluetooth list. Re-clicking the Bluetooth connection will resolve the issue.
    Searching for Bluetooth already connected to the mobile:
	/**
     * Search for connected Bluetooth
     */
    private void findConnectedBluetooth() {
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        Class<BluetoothAdapter> bluetoothAdapterClass = BluetoothAdapter.class; // Get the Class object of BluetoothAdapter
        try { // Get the connection status method
            Method method = bluetoothAdapterClass.getDeclaredMethod("getConnectionState", (Class[]) null);
            // Open permission
            method.setAccessible(true);
            int state = (int) method.invoke(adapter, (Object[]) null);
            if(state == BluetoothAdapter.STATE_CONNECTED){
                LogUtil.logNormalMsg("BLUETOOTH","BluetoothAdapter.STATE_CONNECTED");
                Set<BluetoothDevice> devices = adapter.getBondedDevices();
                LogUtil.logNormalMsg("BLUETOOTH","devices:"+devices.size());
                for(BluetoothDevice device : devices){
                    Method isConnectedMethod = BluetoothDevice.class.getDeclaredMethod("isConnected", (Class[]) null);
                    method.setAccessible(true);
                    boolean isConnected = (boolean) isConnectedMethod.invoke(device, (Object[]) null);
                    if(isConnected){
                        // Found the connected Bluetooth device; proceed to the next step
                        if (device != null) {
                          onDeviceSearchListener.onDeviceFound(device, 50);
                        }
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

(2) The default MTU supported by low-energy Bluetooth GATT for sending data is 23 bytes (byte). Excluding the GATT opcode of one byte and the GATT handle of two bytes, the remaining 20 bytes are reserved for GATT. GATT supports a maximum of 512 bytes of data communication, and the MTU size can be changed by setting it, generally recommended to set it after a successful Bluetooth connection, preferably in the onDescriptorWrite callback method:

		override fun onDescriptorWrite(
            gatt: BluetoothGatt,
            descriptor: BluetoothGattDescriptor,
            status: Int
        ) {
            super.onDescriptorWrite(gatt, descriptor, status)
            if (status == BluetoothGatt.GATT_SUCCESS) {
                // Bluetooth connection configuration successful; MTU can be set now
                mGatt?.requestMtu(103)
            }
        }

The result of whether the MTU is set can be obtained in onMtuChanged:

        override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
            super.onMtuChanged(gatt, mtu, status)
             if (status == BluetoothGatt.GATT_SUCCESS) { 
             	// MTU set successfully; return the currently supported MTU size
				this.supportedMTU = mtu
   			 }
        }

References#

Detailed explanation of Android BLE Bluetooth development
Differences between classic Bluetooth and low-energy (BLE) Bluetooth from a development perspective
Connecting classic Bluetooth in Android
Detailed explanation of Android Bluetooth BLE development

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.