Tìm hiểu về Bluetooth API trên Android : Tạo một ứng dụng Bluetooth Scanner.

Bluetooth Đã trở thành một công nghệ rất phổ biến, đặc biệt là trên các thiết bị di động. Đó là một công nghệ để khám phá và chuyển dữ liệu giữa các thiết bị gần đó. Hầu hết các thiết bị di động hiện đại đều có tính năng Bluetooth . Nếu bạn muốn thực hiện một giao diện ứng dụng với một thiết bị Bluetooth được kích hoạt , ví dụ điện thoại với loa, tai nghe , hoặc các thiết bị điện tử có sẵn kết nối bluetooth ... Bạn phải biết làm thế nào để sử dụng Bluetooth API của Android.

Trong bài này, tôi sẽ hướng dẫn các bạn xây dựng một ứng dụng tương tự như ở trong Bluetooth Settings trong Cài đặt Android. Nó sẽ bao gồm các tính năng sau: - Bật kết nối Bluetooth. - Hiển thị danh sách các thiết bị đã kết nối,ghép đôi (paired). - Tìm và liệt kê danh sách các thiết bị có kết nối Bluetooth ở xung quanh.

Đây giao diện ứng dụng mà chúng ta sẽ làm cho ứng dụng Bluetooth này. Ở cuối bài mình sẽ có link download source của mình để mọi người tìm hiểu. Dưới đây là giao diện ứng dụng mà tôi làm và hướng dẫn các bạn. viblo-image-1.png

1. Bật BLUETOOTH

Từ ứng dụng chúng ta khởi tạo muốn bật tính năng Bluetooth chúng ta cần phải yêu cầu hệ thống cấp cho chúng ta các quyền (uses-permission). Tại file AndroidManifest chúng ta yêu cầu các quyền. Quyền BLUETOOTH 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. Quyền BLUETOOTH_ADMIN 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ị.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="vn.framgia.congnt.bluetoothscanner" >
   <uses-permission android:name="android.permission.BLUETOOTH" />
   <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Sử dụng đối tượng BluetoothAdapter (adapter) để giao tiếp với Bluetooth.Ở đây mình tạo instantiate BluetoothAdapter tại class ListActivity

 BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter();

. Nếu BTAdapter bị null thì có nghĩa là Bluetooth không được hỗ trợ tại thiết bị này. Và ứng dụng sẽ không hoạt động trên thiết bị mà chúng ta đang chạy. Để xử lý việc này thì mình có một solution là hiển thị một AlertDialog để cảnh báo cho người dùng và thoát khỏi ứng dụng.

@Override
protected void onCreate(Bundle savedInstanceState) {
...
    BTAdapter = BluetoothAdapter.getDefaultAdapter();
    // Phone does not support Bluetooth so let the user know and exit.
    if (BTAdapter == null) {
        new AlertDialog.Builder(this)
                .setTitle("Not compatible")
                .setMessage("Your phone does not support Bluetooth")
                .setPositiveButton("Exit", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        System.exit(0);
                    }
                })
                .setIcon(android.R.drawable.ic_dialog_alert)
                .show();
    }
}

Nếu Bluetooth có sẵn trong thiết bị. Chúng ta cần kiểm tra xem nó đã được kích hoạt hay chưa. Nếu chưa được kích hoạt thì chúng ta có thể gửi một action có sẵn được cung cấp bởi Android SDK có tên BluetoothAdapter.ACTION_REQUEST_ENABLE. Khi gửi action này đến hệ thống với một requestCode REQUEST_BLUETOOTH là một số int để xác định yêu cầu kích hoạt, sau đó hệ thống sẽ hiển thị một dialog yêu cầu kích hoạt Bluetooth trên thiết bị.

public class ListActivity extends ActionBarActivity implements DeviceListFragment.OnFragmentInteractionListener  {
    public static int REQUEST_BLUETOOTH = 1;
    ...
    protected void onCreate(Bundle savedInstanceState) {
    ...
        if (!BTAdapter.isEnabled()) {
            Intent enableBT = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableBT, REQUEST_BLUETOOTH);
        }
    }
}

Screenshot_2015-09-24-13-31-02.png

2. Lấy danh sách các thiết bị đã ghép nối (Pair)

Ở bước này, 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).

Một điều quan trọng 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. 2 thiết bị sẽ tự động kết nối khi mã hóa được thành lập. Khi thiết bị đã connected sẽ chia sẻ một kênh RFCOMM cho phép việc gửi và nhận dữ liệu. Một thiết bị có thể có paired với nhiều thiết bị khác, nhưng trong một thời điểm chỉ connected với một thiết bị khác. Một thiết bị Bluetooth được đại diện bởi đối tương BluetoothDevice. Lấy một danh sách các thiết bị đã paired có thể lấy được bằng cách gọi hàm getBondedDevices(). Ở đây mình sẽ gọi hàm getBondedDevices() tại hàm onCreate() của class DeviceListFragment.

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Log.d("DEVICELIST", "Super called for DeviceListFragment onCreate\n");
    deviceItemList = new ArrayList<DeviceItem>();

    Set<BluetoothDevice> pairedDevices = bTAdapter.getBondedDevices();
}

Trong object BlueDevice có rất nhiều getter và setter thể hiện thuộc tính của BluetoothDevice.Ở đây tôi sử dụng hàm getName() để hiển thị tên của thiết bị và getAddress() hiển thị địa chỉ Mac. Địa chỉ Mac là địa chỉ độc nhất để xác định thiết bị. Khi chúng ta có một danh sách các thiết bị. Tôi sẽ tạo ra object DeviceItem tương ứng với BluetoothDevice. Sử dụng một List<DeviceItem> để hiển thị danh sách các thiết bị Bluetooth đã paired với thiết bị trong ứng dụng của tôi.

if (pairedDevices.size() > 0) {
    for (BluetoothDevice device : pairedDevices) {
        DeviceItem newDevice= new DeviceItem(device.getName(),device.getAddress(),"false");
        deviceItemList.add(newDevice);
    }
}

3 Tìm kiếm các thiết bị ở gần có kích hoạt Bluetooth

Ở bước này, Khi bấm nút Scan ứng dụng sẽ tìm kiếm các thiết bị xung quanh chưa kết nối với thiết bị. Các thiết bị có trạng thái unknow. Sau đó thêm chúng vào danh sách thiết bị paired. Code xử lý nằm tại class DeviceListFragment. Đầu tiên thì tôi sẽ tạo một BroadcastReceiver để nhận mỗi khi tìm thấy một thiết bị Bluetooth. Tại method onReceive() có trả về một đối số Intent. Chúng ta có thể kiểm tra action được broadcasting bằng hàm getAction() của Intent. Nếu action là BluetoothDevice.ACTION_FOUND. sau đó ứng dụng sẽ biết là tìm thấy một thiết bị Bluetooth. Khi nhận được action này, chúng ta tạo đối tượng DeviceItem dùng tên thiết bị và địa chỉ Mac. Chúng ta sẽ thêm object DeviceItem vào ArrayAdapter để hiển thị nó trên giao diện ứng dụng.

public class DeviceListFragment extends Fragment implements AbsListView.OnItemClickListener{
    ...
    private final BroadcastReceiver bReciever = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // Create a new device item
                DeviceItem newDevice = new DeviceItem(device.getName(), device.getAddress(), "false");
                // Add it to our adapter
                mAdapter.add(newDevice);
            }
        }
    };
}

Khi nút SCAN bật lên, chỉ cần register BroadcastReceiver và gọi hàm startDiscovery(). Khi nút SCAN tắt đi, chỉ cần unregister BroadcastReceiver và gọi hàm cancelDiscovery(). Việc này rất chiếm tài nguyên của máy. Nếu ứng dụng của bạn kết nối với các thiết bị Bluetooth khác, Bạn nên nhớ phải luôn tắt việc SCAN và sau đó kết nối. Mỗi khi SCAN bắt đầu thì mình sẽ clear ArrayAdapter. Vì khi bắt đầu SCAN chúng ta cũng chẳng muốn hiển thị các thiết bị có thể không còn có trong phạm vi quét của thiết bị.

public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_deviceitem_list, container, false);
    ToggleButton scan = (ToggleButton) view.findViewById(R.id.scan);
    ...
    scan.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
            if (isChecked) {
                mAdapter.clear();
                getActivity().registerReceiver(bReciever, filter);
                bTAdapter.startDiscovery();
            } else {
                getActivity().unregisterReceiver(bReciever);
                bTAdapter.cancelDiscovery();
            }
        }
    });
}

4. Kết nối đến thiết bị bluetooth

Kết nối Bluetooth hoạt động cũng giống với các kết nối khác (Socket,Http...). Phải có một máy chủ(server) và một máy khách(client) và giao tiếp thông qua RFCOMM sockets. Trên Android, RFCOMM sockets được đại diện bởi object BluetoothSocket. May mắn thay!!! Tất cả các kỹ thuật để phát triển một máy chủ(servers) đã được quản lý bởi AndroidSDK và có sẵn thông qua các Bluetooth API. Nên chúng ta không phải quan tâm rằng sẽ phải phát triển một Bluetooth Servers như thế nào 😄(Thề là quá may mắn luôn ý hí hí :v).

Tạo Kết nối như một máy khách(client) thì vô cùng đơn giản. Đầu tiên bạn tạo một RFCOMM Sockets từ BluetoothDevice bằng cách gọi hàm createRfcommSocketToServiceRecord(). truyền một đối số là UUID. Một giá trị 128-bit mà bạn sẽ tạo ra. UUID nó gần giống như là port number (ví dụ giống với web port 80).

Để rõ hơn về vấn đề tôi vừa nói ở trên thì tôi sẽ có một ví dụ. Bạn đang làm một ứng dụng chat mà sử dụng Bluetooth để chat với một người ở gần đó.Để tìm người trò chuyện, bạn sẽ muốn tìm kiếm các thiết bị khác có cài ứng dụng Chat của bạn. Để có thể tìm kiếm được các thiết bị đó, chúng ta sẽ tìm kiếm các UUID trong danh sách dịch vụ của các thiết bị ở gần đó. Sử dụng một UUID để lắng nghe và chấp nhận các kết nối Bluetooth, sau đó sẽ tự động thêm UUID vào danh sách các dịch vụ của điện thoại hoặc dịch vụ phát hiện giao thức.

Khi BluetoothSocket được tạo ra. Bạn gọi method connnect() của đối tượng BluetoothSocket. Việc gọi hàm này sẽ khởi tạo một kết nối với BluetoothDevice thông qua RFCOMM socket.Sau khi thiết bị của chúng ta đã kết nối được với nhau thì sẽ đã có thể trao đổi dữ liệu với các thiết bị được kết nối. Việc duy trì kết nối Bluetooth khá tốn tài nguyên cũng như PIN. Nên chúng ta cần phải đóng socket khi không cần dùng nữa. Để đóng socket chúng ta gọi close() của đối tượng BluetoothSocket. Code ở dưới sẽ thể hiện việc làm thế nào để kết nối đến BluetoothDevice.

public class ConnectThread extends Thread{
    private BluetoothSocket bTSocket;

    public boolean connect(BluetoothDevice bTDevice, UUID mUUID) {
        BluetoothSocket temp = null;
        try {
            temp = bTDevice.createRfcommSocketToServiceRecord(mUUID);
        } catch (IOException e) {
            Log.d("CONNECTTHREAD","Could not create RFCOMM socket:" + e.toString());
            return false;
        }
        try {
            bTSocket.connect();
        } catch(IOException e) {
            Log.d("CONNECTTHREAD","Could not connect: " + e.toString());
            try {
                bTSocket.close();
            } catch(IOException close) {
                Log.d("CONNECTTHREAD", "Could not close connection:" + e.toString());
                return false;
            }
        }
        return true;
    }

    public boolean cancel() {
        try {
            bTSocket.close();
        } catch(IOException e) {
            Log.d("CONNECTTHREAD","Could not close connection:" + e.toString());
            return false;
        }
        return true;
    }
}

Tạo kết nối như một máy chủ thì hơi loằng tằng ngoằng một chút. Thứ nhất, từ BluetoothAdapter của bạn phải tạo được một BluetoothServerSocket, mà sẽ được sử dụng để lắng nghe kết nối. Cái việc này chỉ được sử dụng để thu nhận kết nối 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().

Chúng ta khởi tạo một server socket bằng cách gọi hàm listenUsingRfcommWithServiceRecord (String name, UUID mUUID). Hàm này có 2 tham số name là tên mà chúng ta cung cấp dịch vụ khi nó được thêm vào SDP(Service Discovery Protocol) của điện thoại. UUID phải đúng với UUID của máy khách(client) mà muốn kết nối đến servers.

Sau đó chúng ta gọi accept() ở đối tượng BluetoothServerSocket mới để chờ một kết nối. Khi gọi hàm accept()1 trả về một giá trị nào đó không phải lànull. Chúng ta sẽ gán nó vàoBluetoothSocket` cũ. sau đó chúng ta sẽ sử dụng nó để trao đổi dữ liệu với thiết bị kết nối. Code ở dưới đây tôi hướng dẫn các bạn để accept một kết nối giống như một server:

public class ServerConnectThread extends Thread{
    private BluetoothSocket bTSocket;

    public ServerConnectThread() { }

    public void acceptConnect(BluetoothAdapter bTAdapter, UUID mUUID) {
        BluetoothServerSocket temp = null;
        try {
            temp = bTAdapter.listenUsingRfcommWithServiceRecord("Service_Name", mUUID);
        } catch(IOException e) {
            Log.d("SERVERCONNECT", "Could not get a BluetoothServerSocket:" + e.toString());
        }
        while(true) {
            try {
                bTSocket = temp.accept();
            } catch (IOException e) {
                Log.d("SERVERCONNECT", "Could not accept an incoming connection.");
                break;
            }
            if (bTSocket != null) {
                try {
                    temp.close();
                } catch (IOException e) {
                    Log.d("SERVERCONNECT", "Could not close ServerSocket:" + e.toString());
                }
                break;
            }
        }
    }

    public void closeConnect() {
        try {
            bTSocket.close();
        } catch(IOException e) {
            Log.d("SERVERCONNECT", "Could not close connection:" + e.toString());
        }
    }
}

Để đọc và ghi dữ liệu vào các kết nối chúng ta sử dụng streams, InputStream và OutputStream. Chúng ta có thể tham chiếu đến streams bằng cách gọi getInputStream()getOutputStream() của BluetoothSocket. Đọc và ghi vào streams ta cũng có thể gọi read()write() tương ứng. Đoạn code ở dưới đây sẽ hướng dẫn làm sao để gửi một số nguyên đến thiết bị được kết nối :

public class ManageConnectThread extends Thread {

    public ManageConnectThread() { }

    public void sendData(BluetoothSocket socket, int data) throws IOException{
        ByteArrayOutputStream output = new ByteArrayOutputStream(4);
        output.write(data);
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(output.toByteArray());
    }

    public int receiveData(BluetoothSocket socket) throws IOException{
        byte[] buffer = new byte[4];
        ByteArrayInputStream input = new ByteArrayInputStream(buffer);
        InputStream inputStream = socket.getInputStream();
        inputStream.read(buffer);
        return input.read();
    }
}

Các bạn có thể tìm hiểu các source code Đầy đủ tại Github của mình

Tổng kết : Thông qua bài này chúng ta có thể làm được các tính năng - Quét và hiển thi danh sách các thiết bị Bluetooth xung quanh. - Thiết lập kết nối Bluetooth giữa 2 thiết bị. - Gửi và nhận dữ liệu thông qua kết nối Bluetooth. để có thể triển khai các ứng dụng ví dụ như Chia sẻ dữ liệu thông qua Bluetooth, Gửi tin nhắn qua bluetooth...