Android Bluetooth Development (1) -- Traditional Bluetooth Chat Room

1. Bluetooth Overview

The following is an introduction to Bluetooth, from Wikipedia:

Bluetooth (English: Bluetooth) is a wireless communication technology standard that allows fixed and mobile devices to exchange data over short distances to form a Personal Area Network (PAN). It uses short baud high frequency (UHF) radio waves to communicate via the ISM band from 2.4 to 2.485 GHz [1]. This technology was developed by the telecommunications company Ericsson in 1994 [2]. It was originally designed to create a wireless communication alternative to the RS-232 data line. It can link multiple devices to overcome synchronization issues.

To put it simply, the Bluetooth function supports the device to exchange data with other Bluetooth devices (mobile phones, speakers, etc.) wirelessly.

This chapter also focuses on traditional Bluetooth to learn how to use streaming transmission methods to implement a chat room. The effect is as follows:

client Server

1.1 Basic knowledge

In order for Bluetooth-enabled devices to communicate, they must first be paired to form a communication channel, which can be simply divided into server and client Terminal:

  • Server: It can be detected (searched) and set itself tothe state where it can access requests
  • Client: supports discovery of other detectable Bluetooth devices and requests pairing of devices through connect

When the server receives the pairing, the two devices complete the binding process and exchange security keys during the process, such as the pin key that pops up when two mobile phones are connected via Bluetooth. The two will store the password, and the next time you connect, you can connect automatically without having to go through the above process.

2. Bluetooth development

Next, let’s learn how to use the officially provided Bluetooth to implement Bluetooth development.

2.1 Permission application

First, you need to apply for permission, as follows:

<uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!--
 If your app targets Android 9 or lower, you can declare
         ACCESS_COARSE_LOCATION instead.
    -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

If you want to adapt to Android 9 (API 28) or higher, you need to apply for ACCESS_FINE_LOCATION precise positioning. If it is not suitable, just apply for ACCESS_COARSE_LOCATION.

Note that if it is Android 10, in addition to turning on Bluetooth, you also need to turn on the gps function, otherwise you will not be able to search and connect to other Bluetooth devices
Here is a tip:

 //在 Android 10 还需要开启 gps
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    
    
     val lm: LocationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
     if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)){
    
    
         Toast.makeText(this@MainActivity, "请您先开启gps,否则蓝牙不可用", Toast.LENGTH_SHORT).show()
     }
 }

2.2 Set up Bluetooth

First get BluetoothAdapter. BluetoothAdapter is the entry point for all Bluetooth interactions. With this class, you can discover other Bluetooth devices, query the list of bound devices, and obtain BluetoothDevice, etc. Turning it on is also very simple, as follows:

val bluetooth = BluetoothAdapter.getDefaultAdapter()

If it is null, it means that your device does not have a Bluetooth driver. If found, you can also use bluetooth.isEnabled to determine whether Bluetooth is turned on:

 if (bluetooth == null) {
    
    
      Toast.makeText(this, "您的设备未找到蓝牙驱动!!", Toast.LENGTH_SHORT).show()
      finish()
  }else {
    
    
      if (!bluetooth.isEnabled) {
    
    
          startActivityForResult(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),1)
      }
  }

It will pop up a prompt box asking whether Bluetooth is turned on. As for whether it is turned on successfully, you can use the result in oActivityResult to determine whether it is turned on successfully, such as:

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    
    
     super.onActivityResult(requestCode, resultCode, data)
     if (requestCode == 1){
    
    
         if (resultCode == Activity.RESULT_CANCELED){
    
    
             Toast.makeText(this, "请您不要拒绝开启蓝牙,否则应用无法运行", Toast.LENGTH_SHORT).show()
             finish()
         }
     }
 }

You can also choose to listen for ACTION_STATE_CHANGED broadcast. Whenever the Bluetooth status changes, the system will broadcast this Intent. This broadcast contains extra fieldsEXTRA_STATE and EXTRA_PREVIOUS_STATE, which contain new and old Bluetooth status. These additional fields may have the following values: STATE_TURNING_ON, STATE_ON, a>STATE_OFF and STATE_TURNING_OFF

2.3 Get Bluetooth list

You can get the matched Bluetooth device through BluetoothAdapter, or you can use the startDiscovery() method to find nearby Bluetooth devices. Note that only devices that can be detected can be found.

2.3.1 Get paired Bluetooth devices

Before searching for other devices, you can first get the paired devices through the getBondedDevices() method, which will return a list of BluetoothDevices through which you can get the device's mac address and name, as follows:

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach {
    
     device ->
    val deviceName = device.name
    val deviceHardwareAddress = device.address // MAC address
}

BluetoothDevice represents a Bluetooth device. With this class, you can request to establish a connection with a remote device through BluetoothSocket, or query information about the device, such as connection status, name, address, etc.

2.3.2 Discovering devices

To discover surrounding devices, you can use the startDiscovery() method. This method is an asynchronous operation and returns a boolean value indicating whether the process is successful. The process usually includes a query scan of about 12 seconds. You can register BluetoothDevice.ACTION_FOUND through broadcast, and then pass BluetoothDevice.EXTRA_DEVICE gets the discovered device, as follows:

   /**
     * 查找蓝牙
     */
    fun foundDevices(callback: BlueDevFoundListener?) {
    
    
        blueDevFoundListener = callback;
        //先取消搜索
        bluetoothAdapter.cancelDiscovery()

        //获取已经配对的设备
        val bondedDevices = bluetoothAdapter.bondedDevices
        bondedDevices?.forEach {
    
     device ->
            //公布给外面,方便 recyclerview 等设备连接
            callback?.let {
    
     it(device) }
        }
        //搜索蓝牙,这个过程大概12s左右
        bluetoothAdapter.startDiscovery()
    }

Then in the onReceive() method, press to the other device:

when (intent?.action) {
    
    
     BluetoothDevice.ACTION_FOUND -> {
    
    
         val device =
             intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
         device ?: return
         blueDevFoundListener?.let {
    
     it(device) }
     }
 }

Displayed in recyclerview as follows:
Insert image description here

2.3.3 Launch Detectability

Generally, the Bluetooth of mobile phones can be detected. If you find that the Bluetooth of your mobile phone has not been discovered, or you do not want others to discover the Bluetooth of your mobile phone all the time, you can use the Intent using ACTION_REQUEST_DISCOVERABLE and call startActivityForResult(Intent, int). In this way, you can discard the interest of system detectability mode, so you don't need to configure it in the settings. The default device is in detectable mode for 120 seconds. By adding the EXTRA_DISCOVERABLE_DURATION Extra attribute, you can define different durations, up to 3600 seconds. (1 hour).

If you set the value of the EXTRA_DISCOVERABLE_DURATION Extra property to 0, the device will always be in discoverable mode. This configuration has low security and is highly discouraged.

The following code keeps the device in detectable mode for 5 minutes (300s)

val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
    
    
    putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivityForResult(discoverableIntent,1)

The device will remain in detectable mode for the allocated time, and you can check whether it is turned on successfully in onActivityResult.

If you also want to know the notification of the current mode discovery change, you can register for broadcastACTION_SCAN_MODE_CHANGED, and then receive the fields of BluetoothAdapter< a i=3>EXTRA_SCAN_MODE and EXTRA_PREVIOUS_SCAN_MODE, which provide new and old scan modes respectively. Each Extra attribute may have the following values:

  • SCAN_MODE_CONNECTABLE_DISCOVERABLE : The device is in discoverable mode
  • SCAN_MODE_CONNECTABLE: The device is not in discoverable mode, but can still receive connections.
  • SCAN_MODE_NONE: The device is not in detectable mode and cannot receive connections.

2.4 Connecting devices

If a connection is created between two devices, the server and client mechanisms need to be implemented. One device opens the server socket, and the other device initiates a connection through the mac address. After the connection is successful, the RFCOMM channel will be established. Socket information can be received.

When a device connects to another device, the Android framework will automatically display a request notification box to the user. The RFCOMM request will be blocked until the user clicks on the pairing successfully; or the user refuses the pairing, or times out, and the blocking will disappear.

As shown below:
Insert image description here

2.4.1 Server connection

When two devices are connected, one of them acts as a server. It will monitor whether a client is connected. Create a server through the following steps:

  1. Get the BluetoothserverSocket through listenUsingInsecureRfcommWithServiceRecord() of BluetoothAdapter.
    Its parameters are (String name, UUID uuid), the first one represents the service identifiable name, the system will automatically write to the Service Discovery Protocol (SDP) database entry on the device, the name You can write whatever you want. uuid is more important. You can connect it to the port of the socket. It is the unique identifier of the service. It has 128 bits, so there is basically no need to worry about conflicts; you can use fromString(String) to initialize a UUID. Be sure to keep it consistent with the client (the port is the same)
  2. Call accpet() to listen for connection requests. This method will block consistently and return when receiving or exception occurs. Only when the uuid is the same, the connection will be accepted and the connected BluetoothSocket will be returned.
  3. If you do not accept more connections, you can use the close() method: If you do not plan to connect multiple devices, you can use close(), which will only close the server socket, but the BluetoothSocket socket of accept() will not be closed. Unlike Socket, RFCOMM only allows one connected client to communicate at a time. You can generally call close() Method

So, we can write like this:

/**
     * 监听是否有设备接入
     */
    private inner class AcceptThread(val readListener: HandleSocket.BluetoothListener?,
                                     val writeListener:HandleSocket.BaseBluetoothListener?) : Thread() {
    
    


        private val serverSocket: BluetoothServerSocket? by lazy {
    
    
            //非明文匹配,不安全
            readListener?.onStart()
            bluetooth.listenUsingInsecureRfcommWithServiceRecord(TAG, BlueHelper.BLUE_UUID)
        }

        override fun run() {
    
    
            super.run()
            var shouldLoop = true
            while (shouldLoop) {
    
    
                var socket: BluetoothSocket? =
                    try {
    
    
                        //监听是否有接入
                        serverSocket?.accept()
                    } catch (e: Exception) {
    
    
                        Log.d(TAG, "zsr blue socket accept fail: ${e.message}")
                        shouldLoop = false
                        null
                    }
                socket?.also {
    
    
                    //拿到接入设备的名字
                    readListener?.onConnected(socket.remoteDevice.name)
                    //处理接收事件
                    handleSocket = HandleSocket(socket)
                    handleSocket.start(readListener,writeListener)
                    //关闭服务端,只连接一个
                    serverSocket?.close()
                    shouldLoop = false;
                }
            }
        }

        fun cancel() {
    
    
            serverSocket?.close()
            handleSocket.close()
        }
    }

As you can see, after using listenUsingInsecureRfcommWithServiceRecord(), use accept() to wait for the device to access. After getting the BluetoothSocket, hand it over to HandleSocket to handle reading and writing. We’ll talk about this later

2.4.2 Client

With the server, the client is similar to socket. Its steps are as follows:

  1. Use the createRfcommSocketToServiceRecord(UUID) method of BluetoothDevice to obtain the BluetoothSocket.
    This method initializes the BluetoothSocket object so that the client can connect to the BluetoothDevice. The UUID passed here must match the UUID used by the server device when it called listenUsingRfcommWithServiceRecord(String, UUID) to open its BluetoothServerSocket.
  2. Initiate a connection by calling connect(). Please note that this method is a blocking call.
    When the client calls this method, the system will perform an SDP lookup (as mentioned above on the server side) to find the remote device with the matching UUID. If the lookup is successful and the remote device accepts the connection, it shares the RFCOMM channel, can communicate during the connection, and the connect() method returns the socket. If the connection fails, or the connect() method times out (after approximately 12 seconds), this method throws an IOException.

code show as below:

/**
     * 连接类
     */
    inner class ConnectThread(
        val device: BluetoothDevice, val readListener: HandleSocket.BluetoothListener?,
        val writeListener: HandleSocket.BaseBluetoothListener?
    ) : Thread() {
    
    
        var handleSocket: HandleSocket? = null
        private val socket: BluetoothSocket? by lazy {
    
    
            readListener?.onStart()
            //监听该 uuid 
            device.createRfcommSocketToServiceRecord(BlueHelper.BLUE_UUID)
        }

        override fun run() {
    
    
            super.run()
            //下取消
            bluetooth.cancelDiscovery()
            try {
    
    

                socket.run {
    
    
                    //阻塞等待
                    this?.connect()
                    //连接成功,拿到服务端设备名
                    socket?.remoteDevice?.let {
    
     readListener?.onConnected(it.name) }

                    //处理 socket 读写
                    handleSocket = HandleSocket(this)
                    handleSocket?.start(readListener, writeListener)

                }
            } catch (e: Exception) {
    
    
                readListener?.onFail(e.message.toString())
            }
        }

        fun cancel() {
    
    
            socket?.close()
            handleSocket?.close()
        }
    }

The steps are relatively simple, there’s nothing much to say.

2.4.3 Socket reading and writing

Both the server and the client above use HandleSocket to handle socket reading and writing.
First, you can use BluetoothSocket getInputStream() and getOutputStream() , respectively obtain the InputStream and OutputStream that handle data transmission through the socket.
Then use read(byte[]) and write(byte[ ]) Read data and write it to the data stream.

In this way, our read can be written like this:

/**
     * 读取数据
     */
    private class ReadThread(val socket: BluetoothSocket?,val bluetoothListener: BaseBluetoothListener?) : Thread() {
    
    

        //拿到 BluetoothSocket 的输入流
        private val inputStream: DataInputStream? = DataInputStream(socket?.inputStream)
        private var isDone = false
        private val listener :BluetoothListener? = bluetoothListener as BluetoothListener
        //todo 目前简单数据,暂时使用这种
        private val byteBuffer: ByteArray = ByteArray(1024)
        override fun run() {
    
    
            super.run()
            var size :Int? = null
            while (!isDone) {
    
    
                try {
    
    
                    //拿到读的数据和大小
                    size = inputStream?.read(byteBuffer)
                } catch (e: Exception) {
    
    
                    isDone = false
                    e.message?.let {
    
     listener?.onFail(it) }
                    return
                }


                if (size != null && size>0) {
    
    
                    //把结果公布出去
                    listener?.onReceiveData(String(byteBuffer,0,size))
                } else {
    
    
                    //如果接收不到数据,则证明已经断开了
                    listener?.onFail("断开连接")
                    isDone = false;
                }
            }
        }

        fun cancel() {
    
    
            isDone = false;
            socket?.close()
            close(inputStream)
        }
    }

It's very simple. After getting the inputstream, you can read the data through the read() method. Since it is a chat room, the String is converted directly here. If you want to transfer a file, you can modify it to what you want.

Next is writing

/**
     * 写数据
     */
    class WriteThread(private val socket: BluetoothSocket?,val listener: BaseBluetoothListener?) {
    
    

        private var isDone = false
        //拿到 socket 的 outputstream
        private val dataOutput: OutputStream? = socket?.outputStream
        //暂时现成的线程池
        private val executor = Executors.newSingleThreadExecutor()
        fun sendMsg(msg:String){
    
    
            //通过线程池去发
            executor.execute(sendSync(msg))
        }

        fun cancel(){
    
    
            isDone = true
            executor.shutdownNow()
            socket?.close()
            close(dataOutput)
        }
        //用一个 runnable 去复用
        private inner class  sendSync(val msg:String) :Runnable{
    
    
            override fun run() {
    
    
                if (isDone){
    
    
                    return
                }

                try {
    
    
                    //写数据
                    dataOutput?.write(msg.toByteArray())
                    dataOutput?.flush()

                }catch (e:Exception){
    
    
                    e.message?.let {
    
     listener?.onFail(it) }
                }

            }

        }

    }

As you can see, after getting the outputStream, write the data through write(). Although the official website says that write() generally does not block, we still put it in the thread. Here, we use the thread pool to reuse a runnable and send it directly. Can.

In this way, we have finished learning the chat room function of classic Bluetooth. In the next chapter, we will learn to realize audio transmission between mobile phones and Bluetooth speakers through the A2DP protocol.

Reprint: https://blog.csdn.net/u011418943/article/details/107818438

Guess you like

Origin blog.csdn.net/gqg_guan/article/details/134332283
Recommended