Category: Android

  • ANDROID BLUETOOTH CLASSIC – PART 4

    ANDROID BLUETOOTH CLASSIC – PART 4

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Transfer Bluetooth Data.

    Sau khi bạn đã kết nối thành công với thiết bị Bluetooth, mỗi thiết bị có một BluetoothSocket được kết nối. Sau đó bạn có thể chia sẻ thông tin giữa các thiết bị bằng cách thông qua đối tượng BluetoothSocket, về cơ bản các bước đọc ghi dữ, để truyền dữ liệu như sau:

    • Get InputStream và OutputStream xử lý truyền qua Socket bằng cách sử dụng getInputStream() và getOutputStream() tương ứng.
    • Đọc và ghi dữ liệu vào các luồng sử dụng read(byte[]) và write(byte[]).

    Mình sẽ tạo một ví dụ minh hoạ, với ứng dụng Chat giữa client và server bằng Bluetooth Classic.

    Đầu tiên chúng ta tạo một Object chứa phương thức đọc, ghi. Xử lý Read, Write thông qua 2 object InputStream và OutputStream.

    private val bluetoothAdapter: BluetoothAdapter = adapter
    private val handler: Handler = mHandler
    
    private lateinit var bluetoothSending: BluetoothSending
    
    inner class BluetoothSending(bluetoothSocket: BluetoothSocket?) : Thread() {
    
        private val inputStream: InputStream?
        private val outputStream: OutputStream?
    
        init {
            var tempIn: InputStream? = null
            var tempOut: OutputStream? = null
            try {
                tempIn = bluetoothSocket?.inputStream
                tempOut = bluetoothSocket?.outputStream
            } catch (e: IOException) {
                e.printStackTrace()
            }
            inputStream = tempIn
            outputStream = tempOut
        }
    
        override fun run() {
            val buffer = ByteArray(1024)
            var bytes: Int
    
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    bytes = inputStream?.read(buffer)!!
                    handler.obtainMessage(Contstants.STATE_MESSAGE_RECEIVED, bytes, -1, buffer)
                        .sendToTarget()
                } catch (e: IOException) {
                    Log.e("PhongPN", "Bluetooth Reading Data Error", e)
                    e.printStackTrace()
                }
            }
        }
    
        fun write(bytes: ByteArray?) {
            try {
                outputStream?.write(bytes)
            } catch (e: IOException) {
                Log.e("PhongPN", "Bluetooth Writing Data Error", e)
                e.printStackTrace()
            }
        }
    }
    

    Tiếp đến chúng ta tạo một Server Socket. Lắng nghe các kết nối đến nó.

    inner class ServerClass : Thread() {
    
        private var serverSocket: BluetoothServerSocket? = null
    
        init {
            try {
                serverSocket =
                    bluetoothAdapter.listenUsingRfcommWithServiceRecord(
                        "PhongPN Transfer Data Bluetooth Classic", UUID.fromString(
                            Contstants.UUID
                        )
                    )
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not listen RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            var socket: BluetoothSocket? = null
            while (socket == null) {
                try {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTING
                    handler.sendMessage(message)
                    socket = serverSocket?.accept()
                } catch (e: IOException) {
                    e.printStackTrace()
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTION_FAILED
                    handler.sendMessage(message)
                }
                if (socket != null) {
                    val message = Message.obtain()
                    message.what = Contstants.STATE_CONNECTED
                    handler.sendMessage(message)
                    bluetoothSending = BluetoothSending(socket)
                    bluetoothSending.start()
                    break
                }
            }
        }
    
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Cuối cùng, chúng ta tạo một Client tương tác và trao đổi dữ liệu với Server Socket.

    inner class ClientClass(device: BluetoothDevice) : Thread() {
    
        private var socket: BluetoothSocket? = null
    
        init {
            try {
                socket = device.createRfcommSocketToServiceRecord(UUID.fromString(Contstants.UUID))
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not create RFCOMM Sockets", e)
                e.printStackTrace()
            }
        }
    
        override fun run() {
            try {
                socket?.connect()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTED
                handler.sendMessage(message)
                bluetoothSending = BluetoothSending(socket)
                bluetoothSending.start()
            } catch (e: IOException) {
                e.printStackTrace()
                val message = Message.obtain()
                message.what = Contstants.STATE_CONNECTION_FAILED
                handler.sendMessage(message)
            }
        }
    
        fun cancel() {
            try {
                socket?.close()
            } catch (e: IOException) {
                Log.e("PhongPN", "Could not close the connect socket", e)
                e.printStackTrace()
            }
        }
    }
    

    Chúng ta xử lý send data tới nơi hiển thị dữ liệu (View) qua đối tượng Handle.

    private val handler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(message: Message) {
            when (message.what) {
                Contstants.STATE_LISTENING -> binding.txtStatus.text = "Listening"
                Contstants.STATE_CONNECTING -> binding.txtStatus.text = "Connecting"
                Contstants.STATE_CONNECTED -> binding.txtStatus.text = "Connected"
                Contstants.STATE_CONNECTION_FAILED -> binding.txtStatus.text = "Connection Failed"
                Contstants.STATE_MESSAGE_RECEIVED -> {
                    val readBuff = message.obj as ByteArray
                    val tempMsg = String(readBuff, 0, message.arg1)
                    binding.txtReceivedMessage.text = tempMsg
                }
            }
        }
    }
    

    Các bạn chú ý là phương thức close() của luồng cho phép bạn chấm dứt kết nối bất kỳ lúc nào bằng cách đóng BluetoothSocket. Các nên gọi phương thức này khi bạn sử dụng xong kết nối Bluetooth. Vì thế trong 2 class client vs server mình luôn có 2 hàm đó để sẵn sàng trong việc đóng kết nối.

    Trên đây là chuỗi bài viết của mình liên quan đến Bluetooth Classic trong Android. Mong đâu đấy chút chia sẻ của mình có thế giúp mọi người có cái nhìn sơ qua về Bluetooth Classic, cũng như là có thể ít nhiều giúp các bạn nếu có gặp phải bài toán liên quan đến nó trong tương lai.

    Cảm ơn mọi người đã theo dõi. Hẹn gặp lại trong các bài viết sắp tới.

  • ANDROID BLUETOOTH CLASSIC – PART 3

    ANDROID BLUETOOTH CLASSIC – PART 3

    Chào các bạn, hôm nay mình tiếp tục chuỗi bài liên quan đến Bluetooth Classic trong Android. Bài hôm nay mình tập trung vào phần Connect giữa các thiết bị Bluetooth.

    Để tạo kết nối Bluetooth giữa 2 thiết bị chúng ta cần thiết lập lập kết nối giống như việc kết nối giữa máy chủ (Server) và máy khách (Client). Chúng giao tiếp với nhau thông qua RFCOMM sockets, cụ thể thông qua đối tượng BluetoothSocket Socket trong Android. Cơ chế như nhau, máy khách (Client) cung cấp Socket information khi nó mở một RFCOMM Channel đến máy chủ (Server), máy chủ (Server) nhận Socket information khi kết nối đến được Accept. Server và Client được coi là đã kết nối với nhau khi chúng có một BluetoothSocket được kết nối trên cùng một kênh RFCOMM.

    Có 2 kết nối mà bạn cần quan tâm khi tạo Connect trong Bluetooth Classic

    • Connect như là một Client
    • Connect như là một Server

    Với từng bài toán, từng requirement cụ thể thì ta sẽ sử dụng chúng theo cách phù hợp.

    1. Connect như là một Client

      Cách connect đầu tiên này thường dùng cho bài toán kết nối với một thiết bị Bluetooth có sẵn. Từ đó kết nối, điều khiển, transfer dữ liệu với thiết bị đó. Đầu tiên bạn phải có được một đối tượng BluetoothDevice đại diện cho thiết bị Bluetooth. Đây là công đoạn bạn Scan (Discovery) và lấy thông thiên của thiết bị. Sau đó, bạn phải sử dụng BluetoothDevice để có được BluetoothSocket và bắt đầu kết nối.

      Các bước cụ thể như sau:

      1. Tạo một BluetoothSocket (RFCOMM Sockets) từ BluetoothDevice bằng cách gọi hàm createRfcommSocketToServiceRecord(UUID). Các bạn có hiểu UUID là một mã string, kiểu như một Port Number. Mã này của Client truyền vào hàm này, phải khớp với mã UUID mà máy chủ cấp. Thường với bài toán kết nối với thiết bị Bluetooth, thì thiết bị Bluetooth có cung cấp một chuỗi UUID chuẩn để ứng dụng có thể fix cứng dưới code và gửi lên khi tạo connect.

      2. Gọi BluetoothSocket.connect(). Sau khi gọi hàm, hệ thống sẽ thực hiện tra cứu SDP để tìm thiết bị từ xa có UUID phù hợp. Nếu tìm kiếm và khớp thành công và thiết bị từ xa chấp nhận kết nối, nó sẽ chia sẻ kênh RFCOMM để sử dụng trong quá trình kết nối và trả về phương thức connect(). Nếu kết nối không thành công hoặc nếu phương thức connect() timeout (sau khoảng 12 giây), thì phương thức này sẽ đưa ra một IOException. Bạn có thể tham khảo mã code sau

        class BluetoothClassicConnection(device: BluetoothDevice): Thread() {
        
        private val bluetoothSocket: BluetoothSocket? by lazy {
            device.createRfcommSocketToServiceRecord(UUID)
        }
        
        override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            if (BluetoothAdapter.getDefaultAdapter()?.isDiscovering == true) {
                BluetoothAdapter.getDefaultAdapter()?.cancelDiscovery()
            }
        
            try {
                bluetoothSocket?.let { socket ->
                    socket.connect()
        
                    // Handle Transfer Data 
                }
            } catch (e: IOException) {
                try {
                    bluetoothSocket?.close()
                } catch (e: IOException) {
                    Log.e(TAG, "Could not close the client socket", e)
                }
                Log.e(TAG, "Could not connect", e)
            }
        }
        
        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                bluetoothSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
        
        companion object {
            private const val UUID = "UUID code provide from Server Site"
         }
        }
        

      Note:

      • Việc duy trì kết nối Bluetooth Classic khá tốn tài nguyên của máy, đặc biệt như là PIN. Các bạn cần lưu ý đóng socket khi không cần dùng nữa, bằng cách gọi close() của đối tượng BluetoothSocket. Sau khi gọi Socket sẽ ngay lực tức được đóng lại và giải phóng tất cả các tài nguyên nội bộ có liên quan.
      • Chú ý đến việc huỷ Discovery() trước khi kết nối để đảm bảo việc connect được diễn ra thành công.
    2. Connect như một Server

      Với loại connect này được thực hiện nếu bạn muốn connect 2 thiết bị devices với nhau. Một thiết bị trong 2 thiết bị đó phải hoạt động như một Server bằng cách nắm giữ BluetoothServerSocket. Cụ thể BluetoothServerSocket lắng nghe các kết nối, sử dụng để thu nhận kết nối bluetoothSocket (RFCOMM sockets). Khi kết nối được thiết lập, máy chủ socket không còn cần thiết nữa và có thể đóng lại thông qua hàm close().

      Các bước cụ thể như sau:

      1. Lấy thông tin BluetoothServerSocket bằng cách gọi hàm listenUsingRfcommWithServiceRecord(String, UUID).

      2. Bắt đầu lắng nghe các yêu cầu kết nối bằng cách gọi accept(). Khi thành công, accept() trả về BluetoothSocket được kết nối và sau đó chúng ta sẽ sử dụng nó để trao đổi dữ liệu với thiết bị kết nối.

        class BluetoothClassicServerConnection: Thread() {
        private val serverSocket: BluetoothServerSocket? by lazy {
            BluetoothAdapter.getDefaultAdapter()?.listenUsingInsecureRfcommWithServiceRecord(NAME, UUID)
        }
        
        private var bluetoothSocket: BluetoothSocket? = null
        
        override fun run() {
            // Keep listening until exception occurs or a socket is returned.
            var shouldLoop = true
        
            while (shouldLoop) {
                val socket: BluetoothSocket? = try {
                    bluetoothSocket = serverSocket?.accept()
                } catch (e: IOException) {
                    Log.e(TAG, "Socket's accept() method failed", e)
                    shouldLoop = false
                    null
                }
                socket?.also {
                    
                    // Handle Transfer Data  
        
                    serverSocket?.close()
                    shouldLoop = false
                }
            }
        }
        
        // Closes the connect socket and causes the thread to finish.
        fun cancel() {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
        
        companion object {
            private const val NAME = "The name we provide the service with when it is added to the SDP (Service Discovery Protocol)"
            private const val UUID = "UUID code provide from Server Site"
          }
        }
        

      Note:

      • Khi call accept() trả về BluetoothSocket, Socket đã được kết nối. Vì vậy, không nên gọi connect(), giống như mình làm ở phía Client.

    Vậy là mình cũng vừa trình bày xong phần liên quan đến Connect Bluetooth Classic trong Android. Các loại và các trường hợp sử dụng nó. Bài tiếp theo mình sẽ trình bày về việc Transfer Data (Read and Write) của Bluetooth Classic.

    Hẹn gặp lại các bạn trong bài viết sắp tới.

  • Android Bluetooth Classic – Part 2

    Android Bluetooth Classic – Part 2

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

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

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.

  • Android Bluetooth Classic

    Android Bluetooth Classic

    Tiếp tục trong chuỗi bài liên quan đến Bluetooth Classic trong Android. Mình xin chia sẻ phần liên quan đến Tìm kiếm (Scanning – Discovery) và Enable discoverability thiết bị.

    1. Tìm kiếm (Scanning – Discovery) thiết bị Bluetooth

      Để bắt đầu tìm kiếm (Scanning) thiết bị, hàm startDiscovery() thông qua đối tượng BluetoothAdapter được gọi. Quá trình này bất đồng bộ (asynchronous) và trả về một giá trị boolean cho biết quá trình discovery đã bắt đầu thành công hay chưa. Theo như mình tìm hiểu thì quá trình scan thường được truy vấn trong khoảng 12 giây, sau đó hiển thị output lên màn hình hoặc nơi hiển thị kết quả.

      Để nhận thông tin về từng thiết bị được quét (tìm thấy) thành công, ứng dụng của bạn phải register một BroadcastReceiver với ACTION_FOUND intent. Hệ thống sẽ broadcasts intent này cho từng thiết bị. Trong intent này sẽ chứa công tin về device thông qua các extra fields như EXTRA_DEVICE và EXTRA_CLASS.

      Cụ thể:

    class LegacyPairingFlowBluetoothDeviceReceiver(
        private val mListener: Listener
    ) : BroadcastReceiver() {
    
        companion object {
            private const val RESTART_DISCOVERY_DELAY = 5_000L // ms
        }
    
        private val handler = Handler(Looper.getMainLooper())
        private val restartDiscovery = Runnable {
            if (!cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
        }
    
        var cancelScan: Boolean = false
            set(value) {
                field = value
                if (value) {
                    handler.removeCallbacks(restartDiscovery)
                }
            }
    
        override fun onReceive(context: Context, intent: Intent) {
            val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
            if (state == BluetoothAdapter.STATE_ON && !cancelScan) {
                BluetoothAdapter.getDefaultAdapter()?.startDiscovery()
            }
    
            val action = intent.action
            if (BluetoothDevice.ACTION_FOUND == action) {
                val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice
                if (device != null) {
                    mListener.onFoundBluetoothDevice(device)
                }
            } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED == action) {
                mListener.onDiscoveryStarted()
            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED == action) {
                if (!cancelScan) {
                    handler.postDelayed(restartDiscovery, RESTART_DISCOVERY_DELAY)
                }
                mListener.onDiscoveryFinished()
            }
    
            if (BluetoothAdapter.STATE_ON == state) {
                mListener.onBluetoothStateOn()
            }
        }
    
        interface Listener {
            fun onFoundBluetoothDevice(device: BluetoothDevice)
    
            fun onDiscoveryStarted()
    
            fun onDiscoveryFinished()
    
            fun onBluetoothStateOn() {}
        }
    }
    
    Để bắt đầu kết nối với thiết bị Bluetooth, bạn gọi getAddress() của BluetoothDevice để truy xuất địa chỉ MAC được liên kết.
    

    Note: Thực hiện tìm kiếm (gọi discovery) thiết bị tiêu tốn rất nhiều tài nguyên. Sau khi bạn đã tìm thấy một thiết bị để kết nối, hãy chắc chắn rằng bạn dừng việc tìm kiếm lại bằng function cancelDiscovery() trước khi thử connect với thiết bị đó. Ngoài ra, bạn không nên thực hiện tìm kiếm (gọi discovery) khi được kết nối với thiết bị vì quá trình tìm kiếm làm giảm đáng kể băng thông khả dụng cho bất kỳ kết nối hiện có.

    1. Enable discoverability

      Nếu bạn chỉ cần kết nối từ ứng dụng của bạn với một thiết bị Bluetooth, transfer data với chúng thì bạn không cần phải Enable discoverability của thiết bị. Enable discoverability chỉ cần thiết khi bạn muốn ứng dụng của mình là một host a server socket chấp nhận các kết nối đến, vì các thiết bị Bluetooth phải có thể khám phá các thiết bị khác trước khi khởi động kết nối với các thiết bị khác.

      Để thực hiện điều đó, chúng ta gọi hàm startActivityForResult(Intent, int) với action Intent tương ứng là ACTION_REQUEST_DISCOVERABLE. Theo mặc định, thiết bị có thể được discover trong hai phút. Bạn có thể xác định thời lượng khác, tối đa một giờ, bằng cách thêm EXTRA_DISCOVERABLE_DURATION extra cho Intent đó.

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

    Note: Nếu Bluetooth chưa được bật trên thiết bị, thì việc Enable discoverability sẽ tự động bật Bluetooth.

    Bài tiếp theo mình sẽ trình bày về việc Connect và Transfer giữa ứng dụng và thiết bị Bluetooth. Hẹn gặp lại các bạn ở bài viết sắp tới.

  • Android Bluetooth Classic

    Android Bluetooth Classic

    Tiếp tục chuỗi bài liên quan đến việc tìm hiểu kết nói giữa ứng dụng và thiết bị thông qua Bluetooth. Các bạn có thể tìm hiểu về BLE mình đã chia sẻ tại đây, giống như BLE, hôm nay mình cũng xin phép overview qua và chia sẻ kiến thức mình biết được liên quan đến Bluetooth Classic.

    Chuỗi bài chia sẻ này mình xin chia sẻ một số phần chính

    • Setup Bluetooth Classic trong Android
    • Lấy danh sách các thiết bị ghép nối
    • Tìm kiếm (Scanning) thiết bị
    • Connect và Transfer giữa Ứng dụng và Thiết bị thông qua Bluetooth Classic
    1. Setup Bluetooth Classic trong Android

    Để sử dụng các tính năng Bluetooth trong ứng dụng của mình, bạn phải khai báo vs xin một số quyền để được sử dụng. Tại file AndroidManifest chúng ta yêu cầu các quyền.

    <manifest>
        <!-- Request legacy Bluetooth permissions on older devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH"
                         android:maxSdkVersion="30" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                         android:maxSdkVersion="30" />
    
        <!-- Needed only if your app looks for Bluetooth devices.
             If your app doesn't use Bluetooth scan results to derive physical
             location information, you can strongly assert that your app
             doesn't derive physical location. -->
        <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    
        <!-- Needed only if your app makes the device discoverable to Bluetooth
             devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    
        <!-- Needed only if your app communicates with already-paired Bluetooth
             devices. -->
        <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    
        <!-- Needed only if your app uses Bluetooth scan results to derive physical location. -->
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
        ...
    </manifest>
    

    Như các bạn thấy ở trên:

    • BLUETOOTH permission cho phép ứng dụng kết nối, ngắt kết nối, và truyền dữ liệu với các thiết bị Bluetooth khác.
    • BLUETOOTH_ADMIN permission cho phép ứng dụng phát hiện ra các thiết bị Bluetooth mới và thay đổi cài đặt Bluetooth của thiết bị.
    • BLUETOOTH_SCAN permission phục vụ cho việc Scan thiết bị cho cả Bluetooth Classic và BLE
    • BLUETOOTH_CONNECT cho phép ứng dụng communicates với thiết bị đã được Pair sẵn trong System trước đó
    • BLUETOOTH_ADVERTISE cho phép các thiết bị hiện tại có thể discoverable với các thiết bị khác

    Đối với Android 12 or higher, các bạn chú ý them:

    • Đối với các khai báo quyền liên quan đến Bluetooth cũ, hãy đặt android:maxSdkVersion thành 30. Bước compatibility ứng dụng này giúp hệ thống chỉ cấp cho ứng dụng của bạn các quyền Bluetooth mà ứng dụng đó cần khi cài đặt trên các thiết bị chạy Android 12 trở lên.
    • ACCESS_FINE_LOCATION quyền này chỉ cần thiết nếu ứng dụng của bạn sử dụng kết quả quét Bluetooth để xác định vị trí thực tế. Về quyền này, đặc biệt là các quyền liên quan đến Location, mình sẽ trao đổi rõ hơn ở bài viết tiếp theo liên quan đến Companion Device Manager.

    Note: Trong các quyền trên thì BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE là các runtime permissions. Các bạn chú ý việc xử lý code về việc nhận approve từ người dùng.

    1. Lấy danh sách các thiết bị ghép nối

    Cụ thể mình lấy các thiết bị Bluetooth đã được ghép nối và hiển thị chúng trong một danh sách. Tại các trạng thái mà thiết bị Bluetooth hiển thị trong ứng dụng của mình sẽ là:

    • unknown
    • paired (đã ghép nối).
    • connected (đang kết nối).

    Có một lưu ý là chúng ta cần phải phân biệt giữa paired và connected của một thiết bị Bluetooth. Thiết bị đã paired là chỉ biết về sự tồn tại của nhau và sẵn sàng kết nối thông qua một mã code. Mã code được sử dụng để xác thực và dẫn đến một kết nối. Đầu tiên chúng ta gọi lớp BluetoothAdapter để giao tiếp với Bluetooth. Tạo một object của cuộc gọi bằng cách gọi statics method getDefaultAdapter().

    fun getBluetoothAdapter(context: Context?): BluetoothAdapter? {
        val manager =
            if (context == null) null else context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        return if (manager == null) {
            BluetoothAdapter.getDefaultAdapter()
        } else {
            manager.adapter
        }
    }
    

    Sau khi chúng ta get danh sách Devices đã từng Paired.

    Set<BluetoothDevice> pairedDevices = getBluetoothAdapter(context).getBondedDevices();
    

    Nếu trong trường hợp ứng dụng chưa bật Bluetooth, để cho phép bật Bluetooth của thiết bị, chúng ta gọi intent với hằng số Bluetooth ACTION_REQUEST_ENABLE.

    Intent turnOn = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(turnOn, 0);
    

    Ngoài ra Android còn cung cấp các hằng số khác, các bạn có thể xem phía dưới đây:

    • ACTION_REQUEST_DISCOVERABLE: Hằng số này sử dụng cho việc bật discovering của bluetooth
    • ACTION_STATE_CHANGED: Hằng số này sẽ thông báo rằng trạng thái Bluetooth sẽ được thay đổi
    • ACTION_FOUND: Hằng số này dùng để nhận thông tin về mỗi device mà được discover

    Bài tiếp theo mình sẽ trình bày về việc tìm kiếm Bluetooth Devices, cũng như Connect và Transfer giữa ứng dụng và thiết bị.

  • Annotation in Android Hilt

    Annotation in Android Hilt

    Hôm nay mình xin chia sẻ một chút kiến thức của mình liên quan đến Dependency Injection.

    Đầu tiên mình xin phép nói sơ qua về khái niệm của Dependency Injection. Vậy Dependency Injection là gì ? Cụ thể dựa vào DI trong Principle, Dependency Injection cơ bản là cung cấp đối tượng phụ thuộc như 1 tham số. Ứng dụng phụ thuộc này khi các class đã được xây dựng hoặc chuyển chúng (các đối tượng phụ thuộc) vào các function cần mỗi phụ thuộc. Cách triển khai chúng theo lý thuyết là lấy đấy tượng phụ thuộc và cung cấp chúng vào class thay vì tạo ra thể hiện của chúng trực tiếp trong class bị phụ thuộc.

    Ưu điểm:

    • Khả năng tái sử dụng lại các class và tách rời các phụ thuộc (dependencies): Dễ dàng hơn trong việc thay đổi một dependency. Việc tái sử dụng code được cải thiện do Inversion of Control, và các class không còn kiểm soát việc tạo ra các dependencies như thế nào, thay vào đó nó có thể làm việc với bất kỳ cấu hình nào.
    • Dễ tái cấu trúc: Các dependencies trở thành những phần có thể kiểm tra như API. Hoàn toàn có thể kiểm tra lúc tạo đối tượng, lúc biên dịch chứ không bị ẩn đi.
    • Dễ dàng cho việc Testing: Các class không quản lý các dependencies của nó. Vì thế khi testing chúng ta có thể truyền các dependency khác nhau và xử lý được nhiều test case

    Tiếp đến mình sẽ nói phần chính của bài này liên quan đến Annotation in Android Hilt (Một framework trong Android support việc triển khai Dependency Injection trong Project).

    Chúng ta có 3 annotation thường dùng để inject các object trong Hilt:

    • @Inject: annotation dùng ở constructor của class
    • @Provides: annotation dùng ở Module
    • @Binds: một annotation khác cũng dùng ở Module

    Vậy khi nào dùng chúng, đặc biệt là giữa @Provide và @Binds?

    1. Inject

    Chúng ta dùng @Inject annotation ở tất cả các constructor mà mình cần inject đối tượng (Object). Ví dụ như ViewModel, Repository, UseCase, DataSource, thậm trí là Android classes (ex. Activity, Fragment, …)

    class ConnectRepositoryImpl @Inject constructor(
        private val connectManager: ConnectManager
    ) : ConnectRepository {
    
       fun doSomething() {}
    }
    
        class ConnectUseCaseImpl @Inject constructor(
            private val connectRepository: ConnectRepository,
        ) : ConnectUseCase {
    
        fun doSomething() {
            // Using connectRepository 
        }
     }
    

    Qua ví dụ trên sau khi khởi tạo class ConnectRepositoryImpl với @Inject constructor. Chúng ta có thể dễ dàng sử dụng ConnectRepository ở các class khác (như ViewModel, Android class, UseCase) bằng cách inject chúng vào nơi cần chúng. Tuy nhiên thì chúng ta lại chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define.

    1. Provide

    Vậy thì để khắc phục hạn chế chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define của @Inject, inject object của những class mà mình không define (Ví dụ như Retrofit, OkHttpClient hoặc Room database), chúng ta cùng đến với @Provides. Trước tiên, chúng ta cần tạo một @Module để chứa các dependency với annotation @Provides.

        @InstallIn(SingletonComponent::class)
        @Module
        class DatabaseModule {
    
            @Provides
            @Singleton
            fun provideGSDatabase(@ApplicationContext context: Context): GSDatabase {
                return Room.databaseBuilder(context, GSDatabase::class.java, GS_DATABASE_NAME)
                    .fallbackToDestructiveMigration()
                    .build()
            }
    
            @Provides
            fun provideSpeakerDao(gsDatabase: GSDatabase): SpeakerDao {
                return gsDatabase.speakerDao()
            }
        }
    

    Như đoạn code ở trên, mình khởi tạo đối tượng RomDatabase và một đối tượng Dao của chúng,  đó là khởi tạo object không phải code của chúng ta define, hơn nữa trong trường hợp này còn khởi tạo theo kiểu Builder pattern, nên chúng ta không thể dùng @Inject annotation mà bắt buộc phải dùng @Provides. Bây giờ, chúng ta đã có thể inject object của interface GSDatabase và SpeakerDao ở bất cứ đâu.

    1. Bind

    Đối với interface, chúng ta không thể dùng annotation @Inject, vì nó không có constructor function. Tuy nhiên, nếu bạn có một interface mà chỉ có duy nhất một implementation (một class implement interface đó), thì bạn có thể dùng @Binds để inject interface đó. Việc inject interface thay vì class là trong những cách triển khai được Recommend sử dụng bởi vì nó tuân theo nguyên tắc Dependence Inversion của SOLID, từ đó dễ dàng cho việc testing và maintain mai này.

    interface ConnectRepository {
        fun doSomething()
    }
    
    class ConnectRepositoryImpl @Inject constructor(
        private val connectManager: ConnectManager
    ) : ConnectRepository {
    
       fun doSomething() {}
    }
    
    @Module
    @InstallIn(SingletonComponent::class)
    abstract class ConnectDeviceModule {
        @Binds
        @Singleton
        abstract fun provideConnectRepository(
            connectRepository: ConnectRepositoryImpl
        ): ConnectRepository
    }
    
    class ConnectUseCaseImpl @Inject constructor(
        private val connectRepository: ConnectRepository,
    ) : ConnectUseCase {
    
      fun doSomething() {
          // Using connectRepository
      }
    }
    

    Cụ thể trong ví dụ trên, mình khai báo 1 interface ConnectRepository thể hiện cho đối tượng ConnectRepositoryImpl. Từ đó khi sử dụng ConnectRepositoryImpl chúng ta sẽ sử dụng và gọi nó thông qua ConnectRepository.

    Ưu điểm của việc dùng @Binds thay cho @Provides là nó giúp giảm lượng code được generate, như là Module Factory class. Từ đó tăng thời gian build và tăng kích thước file .apk và .aab của Project.

    Trên đây là chia sẻ của mình về 1 số Annotation của Hilt Android. Mong sẽ giúp ích được ít nhiều mọi người. Hẹn mọi người ở bài viết sắp tới

  • Annotation in Android Hilt

    Annotation in Android Hilt

    Hôm nay mình xin chia sẻ một chút kiến thức của mình liên quan đến Dependency Injection.

    Đầu tiên mình xin phép nói sơ qua về khái niệm của Dependency Injection. Vậy Dependency Injection là gì ? Cụ thể dựa vào DI trong Principle, Dependency Injection cơ bản là cung cấp đối tượng phụ thuộc như 1 tham số. Ứng dụng phụ thuộc này khi các class đã được xây dựng hoặc chuyển chúng (các đối tượng phụ thuộc) vào các function cần mỗi phụ thuộc. Cách triển khai chúng theo lý thuyết là lấy đấy tượng phụ thuộc và cung cấp chúng vào class thay vì tạo ra thể hiện của chúng trực tiếp trong class bị phụ thuộc.

    Ưu điểm:

    • Khả năng tái sử dụng lại các class và tách rời các phụ thuộc (dependencies): Dễ dàng hơn trong việc thay đổi một dependency. Việc tái sử dụng code được cải thiện do Inversion of Control, và các class không còn kiểm soát việc tạo ra các dependencies như thế nào, thay vào đó nó có thể làm việc với bất kỳ cấu hình nào.
    • Dễ tái cấu trúc: Các dependencies trở thành những phần có thể kiểm tra như API. Hoàn toàn có thể kiểm tra lúc tạo đối tượng, lúc biên dịch chứ không bị ẩn đi.
    • Dễ dàng cho việc Testing: Các class không quản lý các dependencies của nó. Vì thế khi testing chúng ta có thể truyền các dependency khác nhau và xử lý được nhiều test case

    Tiếp đến mình sẽ nói phần chính của bài này liên quan đến Annotation in Android Hilt (Một framework trong Android support việc triển khai Dependency Injection trong Project).

    Chúng ta có 3 annotation thường dùng để inject các object trong Hilt:

    • @Inject: annotation dùng ở constructor của class
    • @Provides: annotation dùng ở Module
    • @Binds: một annotation khác cũng dùng ở Module

    Vậy khi nào dùng chúng, đặc biệt là giữa @Provide và @Binds?

    1. Inject

    Chúng ta dùng @Inject annotation ở tất cả các constructor mà mình cần inject đối tượng (Object). Ví dụ như ViewModel, Repository, UseCase, DataSource, thậm trí là Android classes (ex. Activity, Fragment, …)

    <pre> class ConnectRepositoryImpl @Inject constructor( private val connectManager: ConnectManager ) : ConnectRepository {

    fun doSomething() {} } </pre>

    <pre> class ConnectUseCaseImpl @Inject constructor( private val connectRepository: ConnectRepository, ) : ConnectUseCase {

    fun doSomething() { // Using connectRepository } } </pre>

    Qua ví dụ trên sau khi khởi tạo class ConnectRepositoryImpl với @Inject constructor. Chúng ta có thể dễ dàng sử dụng ConnectRepository ở các class khác (như ViewModel, Android class, UseCase) bằng cách inject chúng vào nơi cần chúng. Tuy nhiên thì chúng ta lại chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define.

    1. Provide

    Vậy thì để khắc phục hạn chế chỉ có thể sử dụng annotation này để annotate constructor của những class mà mình tự define của @Inject, inject object của những class mà mình không define (Ví dụ như Retrofit, OkHttpClient hoặc Room database), chúng ta cùng đến với @Provides. Trước tiên, chúng ta cần tạo một @Module để chứa các dependency với annotation @Provides.

    <pre> @InstallIn(SingletonComponent::class) @Module class DatabaseModule {

    @Provides
    @Singleton
    fun provideGSDatabase(@ApplicationContext context: Context): GSDatabase {
        return Room.databaseBuilder(context, GSDatabase::class.java, GS_DATABASE_NAME)
            .fallbackToDestructiveMigration()
            .build()
    }
    
    @Provides
    fun provideSpeakerDao(gsDatabase: GSDatabase): SpeakerDao {
        return gsDatabase.speakerDao()
    }
    

    } </pre>

    Như đoạn code ở trên, mình khởi tạo đối tượng RomDatabase và một đối tượng Dao của chúng,  đó là khởi tạo object không phải code của chúng ta define, hơn nữa trong trường hợp này còn khởi tạo theo kiểu Builder pattern, nên chúng ta không thể dùng @Inject annotation mà bắt buộc phải dùng @Provides. Bây giờ, chúng ta đã có thể inject object của interface GSDatabase và SpeakerDao ở bất cứ đâu.

    1. Bind

    Đối với interface, chúng ta không thể dùng annotation @Inject, vì nó không có constructor function. Tuy nhiên, nếu bạn có một interface mà chỉ có duy nhất một implementation (một class implement interface đó), thì bạn có thể dùng @Binds để inject interface đó. Việc inject interface thay vì class là trong những cách triển khai được Recommend sử dụng bởi vì nó tuân theo nguyên tắc Dependence Inversion của SOLID, từ đó dễ dàng cho việc testing và maintain mai này.

    <pre> interface ConnectRepository { fun doSomething() } </pre>

    <pre> class ConnectRepositoryImpl @Inject constructor( private val connectManager: ConnectManager ) : ConnectRepository {

    fun doSomething() {} } </pre>

    <pre> @Module @InstallIn(SingletonComponent::class) abstract class ConnectDeviceModule { @Binds @Singleton abstract fun provideConnectRepository( connectRepository: ConnectRepositoryImpl ): ConnectRepository } </pre>

    <pre> class ConnectUseCaseImpl @Inject constructor( private val connectRepository: ConnectRepository, ) : ConnectUseCase {

    fun doSomething() { // Using connectRepository } } </pre>

    Cụ thể trong ví dụ trên, mình khai báo 1 interface ConnectRepository thể hiện cho đối tượng ConnectRepositoryImpl. Từ đó khi sử dụng ConnectRepositoryImpl chúng ta sẽ sử dụng và gọi nó thông qua ConnectRepository.

    Ưu điểm của việc dùng @Binds thay cho @Provides là nó giúp giảm lượng code được generate, như là Module Factory class. Từ đó tăng thời gian build và tăng kích thước file .apk và .aab của Project.

    Trên đây là chia sẻ của mình về 1 số Annotation của Hilt Android. Mong sẽ giúp ích được ít nhiều mọi người. Hẹn mọi người ở bài viết sắp tới

  • Android Bluetooth Low Energy (BLE) – Part 3

    Android Bluetooth Low Energy (BLE) – Part 3

    Chào các bạn, tiếp tục loạt bài vè chủ đề BLE, hôm nay mình tiếp tục trình bày về Discovery Service và Transfer BLE Data

    1. Discovery Service Sau khi kết nối thành công, để phục vụ việc Transfer dữ liệu, điều đầu tiên cần làm khi bạn kết nối với GATT Server trên thiết bị BLE là thực hiện Discovery Service. Điều này cung cấp thông tin về các Available Service trên remote device cũng như các service characteristics và thông tin của chúng. Cụ thể là

      image

      image

      image

      3 thông số trên thì device sẽ có một mã code riêng phục vụ việc Discovery và mở cổng transfer dữ liệu giữa App và thiết bị.

    2. Turning notifications on and off

      Để thực hiện việc Read và Write dữ liệu giữa ứng dụng và Device BLE chúng ta cần Turning notifications sau khi Discovery Service thành công. Cụ thể là việc Call setCharacteristicNotification(). Điều này các bạn hiểu đơn giản là sẽ báo cho ngăn xếp Bluetooth về cái mong đợi nhận thông báo cho characteristic cụ thể của Device BLE đó. Nếu không có điều này hoàn bạn bạn không thể nhận được dữ liệu response từ Device BLE cho dù bạn có thể gọi thành công writeCharacteristic.

      Cụ thể về việc gọi Turning notifications. Chú ý về việc thực hiện sau khi Discovery Service thành công. Bạn có thể làm điểu này bằng Callback. Nhưng trong ví dụ của mình thì mình sử dụng 1 SingleSubject của Rx để thực hiện nó. 

      image

      image

      image  

    3. Read and Write dữ liệu

      Đầu tiên các bạn clear rằng, kiểu dữ liệu giao tiếp giữa ứng dụng và Device BLE là ByteArray. Và để nhận biết việc nhận gửi đó giữa ứng dụng và Device BLE nào thì nó sẽ thông qua outputCharacteristicinputCharacteristic tương ứng với việc Write và Read. 2 thông số outputCharacteristicinputCharacteristic lấy được từ đối tượng BluetoothGattService khi sau khi chúng ta Discovery Service.

      Data ByteArray gửi lên được define cụ thể cho từng Device BLE cụ thể. Đa phần sẽ thuộc về team Hardware và Firmware cung cấp file command request và response tương ứng của thiết bị đó.

      Có 1 điểm lưu ý là trong quá trình trao đổi dữ liệu giữa ứng dụng và Device BLE, chúng ta sẽ gửi nhận liên tục. Hoàn toàn bài toán của việc gửi dữ liệu, Device BLE chưa trả về và mình lại đã gửi tiếp dữ liệu 1 lần nữa. Để giải quyết bài toán này, các bạn nên quản lý việc gửi nhận dữ liệu (hay ở đây mình hay gọi là command request ) trong 1 Queue tương ứng. Điều đó sẽ giúp các bạn quản lý dễ dàng cho dù bạn muốn việc thực hiện tuần tự, hay loại nào khác cũng sẽ dễ dàng hơn.

      image

    • Đây là quá trình gửi dữ liệu từ ứng dụng sang Device BLE. Thông qua việc gọi writeCharacteristic qua đối tượng bluetoothGatt.

      image

    • Sau khi gọi thành công. System sẽ trả về hàm callback tương ứng, thông về status đã gửi thành công hay thất bại đến Device BLE.

      image

    • Tiếp đến là quá trình nhận dữ liệu sau khi request gửi đến Device BLE thành công. Thiết bị sẽ trả về response tương ứng tại 1 trong 2 functions.

      image  Sau khi nhận được dự liệu kiểu ByteArray. Chúng ta sẽ thực hiện việc xử lý dữ liệu tương ứng.

    Lưu ý:

    Vì BLE là asynchronous nên là có nhiều Thread được tạo ra và thực thi. Cụ thể ở đây là  khi nhận callbacks on BluetoothGattCallback, thì các dòng code này sẽ thực thực hiện **Binder** threads. Các hàm mình muốn nói đến ở đây ví dụ như các hàm **onCharacteristicWrite()**, **onCharacteristicRead()**, **onCharacteristicChanged()**, … đây là 1 số functions quan trọng trong việc xử lý và thực thi gửi nhận dữ liệu. 
    
    Vậy tại sao nên tránh xử lý trên Binder threads? Khi bạn nhận được nội dung nào đó trên **Binder** Thread, Android sẽ không gửi bất kỳ lệnh gọi lại mới nào cho đến khi code của bạn hoàn thành trên **Binder** Thread. Vì vậy, nói chung, bạn nên tránh thực hiện nhiều thao tác trên các luồng **Binder** vì bạn đang chặn các cuộc gọi lại mới khi bạn đang sử dụng nó.
    

    Đây là những chia sẻ mang tính overview của mình về BLE của Android. Mong ít nhiều có thể chia sẻ cho mọi người.

  • Android Bluetooth Low Energy (BLE) – Part 2

    Android Bluetooth Low Energy (BLE) – Part 2

    Chào các bạn, tiếp tục loạt bài vè chủ đề BLE, hôm nay mình tiếp tục trình bày về Connection, Disconnect BLE

    1. Connection

    Sau khi bạn đã Scan thấy thiết bị của mình bằng cách quét tìm thiết bị, bạn phải kết nối với thiết bị đó bằng cách gọi connectGatt(). Nó trả về một đối tượng BluetoothGatt mà sau đó bạn sẽ sử dụng cho tất cả các hoạt động liên quan đến GATT như đọc ghi dữ liệu thông qua BLE (Read and Write). Các bạn chú ý, có 2 version của phương thức connectGatt()

    image

    Hay cụ thể hơn

    image

    Nào chúng ta cùng đi vào sâu hơn một số đối số trong hàm Connect này.

    1. “autoConnect” parameter: Đối số chỉ ra rằng bạn có muốn kết nối ngay lập tức hay không.
    • Trong trường hợp này mình set giá trị là “False” tức là ‘connect immediately’. Theo mình tìm hiểu, Android sẽ cố gắng connect trong 30s, nếu không thành công đẩy ra lỗi Timeout (Thường với mã lỗi là 133). Có một lưu ý là bạn chỉ có thể tạo một kết nối tại một thời điểm bằng cách sử dụng false, vì Android sẽ hủy mọi kết nối khác có giá trị false, nếu có.
    • Vậy nếu mình set là “True” thì sao nhỉ? Android sẽ kết nối bất cứ khi nào nó nhìn thấy thiết bị và cuộc gọi này sẽ không bao giờ Timeout. Theo mình tìm hiểu, bên trong stack sẽ tự quét và khi nhìn thấy thiết bị, nó sẽ kết nối với thiết bị đó. Bạn có thể cân nhắc đến bài toán Reconnect device trong trường hợp này. Bạn chỉ cần tạo một đối tượng BluetoothDevice và gọi connectGatt với value là “True” tương ứng với đối số autoConnect.
    1. “transport” parameter: Đưa ra các mode cho việc connect giữa Device và ứng dụng. Nếu trong trường hợp của việc kết nối BLE thì các bạn nên sử dụng value TRANSPORT_LE để tránh những lỗi không mong muốn trong quá trình connect. Có một giá trị mà các bạn cũng cần lưu ý là TRANSPORT_AUTO, hỗ trợ cho việc connection giữa điện thoại và thiết bị hỗ trợ kết nối cả BLE và Bluetooth classic.

    Sau khi mình gọi việc connect, System sẽ đẩy kết quả và các callback tương ứng thông qua BluetoothGattCallback

    image

    2. Disconnection

    Mình có tìm hiểu có rất nhiều source mẫu handle việc gọi disconnect BLE sai cách. Sau đây là các đúng mà mình tìm hiểu được nếu bạn muốn Disconnect BLE

    • Call disconnect()
    • Đợi Callback onConnectionStateChange với trạng thái Disconnect
    • Call close()
    • Dispose gatt object

    image

    image

    Lệnh disconnect() thực sự sẽ thực hiện ngắt kết nối và cũng sẽ cập nhật trạng thái kết nối nội bộ của ngăn xếp Bluetooth. Sau đó, nó sẽ kích hoạt gọi lại onConnectionStateChange để thông báo cho bạn rằng trạng thái mới hiện đã bị ‘ngắt kết nối’.

    Lệnh gọi close() sẽ hủy đăng ký BluetoothGattCallback của bạn và giải phóng ‘client interface’.

    Cuối cùng, việc xử lý đối tượng BluetoothGatt sẽ giải phóng các tài nguyên khác liên quan đến kết nối.

    Ở biết tiếp theo mình sẽ tiếp tục với việc Discovery Service BLE sau khi Connection thành công và Transfer BLE Data của BLE. Hẹn các bạn trong bài viết sắp tới.

  • Android Bluetooth Low Energy (BLE)

    Android Bluetooth Low Energy (BLE)

    Chào các bạn, hiện tại mình đang làm một dự án liên quan đến BLE (Bluetooth Low Energy). Mình tìm trên mạng thì thấy khá nhiều tài liệu liên quan đến Bluetooth Classic, còn tài liệu về BLE thì còn hạn chế hơn rất nhiều. Nhưng mình lại để ý rằng các ứng dụng phục vụ kết nối với thiết bị thì hiện tại sử dụng BLE là một giải pháp kết nối là khá thông dụng. Vậy nên hôm nay mình sẽ chia sẻ một chút kiến thức cơ bản liên quan đến chủ đề này. Đâu đấy sẽ giúp các bạn có thể Overview qua về hướng tiếp cận, để từ đó dễ dàng investigate và triển khai theo yêu cầu.

    Bài của mình liên bao gồm những phần chính sau:

    1. Giới thiệu về BLE
    2. Find BLE Device
    3. Connection and Disconnect BLE Device
    4. Transfer BLE Data

    Let’s start

    1. Giới thiệu về BLE

    Đặt vấn đề, trong hầu hết các trường hợp, các nhà thiết kế thiết bị có thể đeo, ngoại vi, cũng như tất cả các mặt hàng khác cần mở rộng chức năng của chúng với điện thoại thông minh. Đều cần tìm một giải pháp để kết nối, điều khiển và chia sẻ dữ liệu. Và Bluetooth Classic và BLE chính là giải pháp. Trong phạm trù bài chia sẻ hôm nay mình chỉ tập trung vào nền tảng Android. Như các bạn đã biết, Bluetooth Classic cho phép thiết bị trao đổi dữ liệu không dây với các thiết bị Bluetooth khác. Khung ứng dụng cung cấp quyền truy cập vào chức năng Bluetooth thông qua API Bluetooth. Các API này cho phép các ứng dụng kết nối với các thiết bị Bluetooth khác, cho phép các tính năng không dây điểm-điểm và đa điểm.

    Android version 4.3 (API 18) and above BLE ra đời. BLE được thiết kế để tiêu thụ điện năng thấp hơn đáng kể. Điều này cho phép các ứng dụng giao tiếp với các thiết bị BLE có yêu cầu năng lượng nghiêm ngặt hơn. Cụ thể: Các trường hợp sử dụng phổ biến bao gồm:

    • Truyền một lượng nhỏ dữ liệu giữa các thiết bị lân cận.
    • Tương tác với các cảm biến tiệm cận để cung cấp cho người dùng trải nghiệm tùy chỉnh dựa trên vị trí hiện tại của họ.

    2. Find BLE Device

    Đây là bước đầu tiên trước khi sử dụng các tính năng Bluetooth Classic hoặc BLE. Các bạn cần đảm bảo rằng tất cả các quyền và tính năng cần thiết đều được áp dụng và cho phép.

    Permission:

    • android.permission.BLUETOOTH – các tính năng Bluetooth classic và BLE cơ bản
    • android.permission.BLUETOOTH_ADMIN – các thao tác BC và BLE nâng cao như bật/tắt module _ Bluetooth, discovery device, ….
    • android.permission.ACCESS_COARSE_LOCATION – cần thiết để quét BLE trên Android 5.0 (API 21) trở lên. Lưu ý: Đây là Runtime Permission từ API 23.

    Mình có đọc thêm chú ý về một quyền mà mình thấy hay được khai báo khi xử lý vs BLE. Các bạn chú ý cũng cần đảm bảo rằng điện thoại hay Tablet có bộ điều hợp Bluetooth tích hợp. Nếu muốn không khả dụng cho các thiết bị không có Bluetooth, chúng tôi chỉ cần thêm một khai báo:

    <uses-feature android:name="android.hardware.bluetooth"/>
    

    Tuy nhiên, theo Document mình tìm hiểu, không bắt buộc nếu khai báo quyền Bluetooth và đặt phiên bản Android 5 trở lên.

    image

    Để tìm các thiết bị BLE, bạn sử dụng startScan()stopScan(). Phương thức này lấy leScanCallback làm tham số. Bạn phải triển khai lệnh gọi lại này vì đó là cách trả về kết quả quét. Chú ý đến việc optimize khi thực thi, ví dụ stopScan() khi đã tìm thấy device mà các bạn muốn hoặc đặt một interval time scan nhất định …

    image

    Bạn cũng có thể cân nhắc đến mode khi Scan trong trường hợp xuống Background và lên ForceGround tương ứng để optimize năng lượng của thiết bị.

    image

    Ở biết tiếp theo mình sẽ tiếp tục với việc Connection, Disconnect BLE Device và Transfer BLE Data của BLE.

    Hẹn các bạn trong bài viết sắp tới.