asd asdsa f
Bluetooth для Андроид
Статьи

Как разработать приложение с Bluetooth для Андроид

7 3242

Bluetooth для андроид — как разработать приложение, которое позволяется взаимодействовать с другими устройствами по buletooth? Об этом пойдет речь в данной статье. Вы узнаете, как:

  • Инициализировать поиск устройств, поддерживающих подключения по bluetooth
  • Подключиться к android-устройству
  • Отправлять и получать сообщения от андроид-устройства по bluetooth

Начнем с описания функциональности тестового приложения.

Описание работы приложения bluetooth для Андроид

Тестовое приложение в этой статье позволяет подключаться к устройствам, на котором уже запущенно это же приложение. Как это работает?

Приложение использует т.н. Bluetooth SDP (service discovery protocol). Если вкратце, то этот протокол обеспечивает общение по bluetooth. В протоколе SDP сетевой сервис, который предоставляет приложение, идентифицируется с помощью UUID (уникального идентификатора). SDP позволяет подключаться и принимать подключения только с тем же UUID. Как реализовать такое подключения и идет речь в статье.

Конечно, для тестирование приложения понадобятся минимум два устройства с bluetooth и Android. 

Вот основные возможности будущего приложения:

  • Поиск устройств со включенным bluetooth.
  • Подключение к устройству\прием входящего соединения от стороннего устройства.
  • Отправка сообщения подключенному устройству

Интерфейс состоит из одной активности с таким интерфейсом:

Интерфейс пользователя приложения bluetooth Аднроид

Порядок работы с приложением:

1) Нажмите «Сделать доступным для поиска». Запустится активность для подтверждения перевода устройства в состояние «доступнен для поиска».

02_zapros_na_dostupnost_dlya_poiska

2) Нажмите «Найти устройства». Произойдет запрос к адаптеру bluetooth для поиска устройств:

03_pjisk_ustroystv

3) Как только поиск будет завершен, вы увидите все доступные устройства c bluetooth. У меня постоянно всплывал, помимо второго тестового смартфона, еще и соседский телевизор.

04_naydelliye_ustroystva

4) Нажмите на устройство, на котором уже запущено тестовое приложение.

5) Появится Toast с сообщением о том, что устройство подключено.

6) Введите сообщение в поле под кнопкой «Отправить сообщение», и нажмите эту кнопку.

7) Проверьте, что на втором устройстве отобразилось полученное сообщение.

Тестовое приложение очень простое в плане функциональности. Однако, всего, что в нем использовано, будет вполне достаточно для построения любого интерактивного приложения bluetooth.

Обзор проекта bluetooth-приложения под Android

Файл проекта: MainActivity, ClientThread,  Communicator, CommunicatorImpl, CommunicatorService,  ServerThread

  • MainActivity — файл активности. Содержит обработчики нажатий на кнопки и код запуска подключения к устройству и прием входящих подключений
  • ClientThread — запускает процесс общения с устройством
  • ServerThread — ждет входящих подключений и обрабатывает полученные от устройства сообщения

Далее идут вспомогательные классы для обработки сообщений.

  • Communicator — интерфейс, описывающий процесс общения между устройствами
  • CommunicatorService — интерфейс фабрики для создания Communicator-ов (см. код MainActivity ниже)
  • CommunicatorImpl — простая реализация Communicator-а

Разрешения для BLUETOOTH

Практически любая операция работы с bluetooth в Андроиде требует двух разрешений в файле манифеста приложения. Следующие строки должны присутствовать в файле AndroidManifest.xml:

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

android.permission.BLUETOOTH_ADMIN — позволяет управлять настройками bluetooth, искать и подключать устройства. В свою очередь, BLUETOOTH_ADMIN требует разрешения BLUETOOTH.

Интерфейс приложения, главная активность MainActivity

Разметка интерфейса главной активности выглядит следующим образом:
Посмотреть код


<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:focusable="true" 
        android:focusableInTouchMode="true" >

        <Button
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:onClick="makeDiscoverable"
            android:text="@string/title_make_discoverable" />

        <Button
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:onClick="discoverDevices"
            android:text="@string/title_discover_devices" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/title_tap_to_connect" />

        <ListView
            android:id="@id/android:list"
            android:layout_width="fill_parent"
            android:layout_height="120dp" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/prompt_enter_message" />

        <Button
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:onClick="sendMessage"
            android:text="@string/title_send_message" />

        <EditText
            android:id="@+id/message_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:hint="@string/title_message_for_device" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="@string/title_message_received" />

        <TextView
            android:id="@+id/data_text"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</ScrollView>

Заметьте, что обработчики нажатий указаны прямо в разметке с помощью атрибута android:onClick, куда передается имя метода в активности. Сигнатура метода подразумевает единственный параметр View. Например:

    public void sendMessage(View view) {
      //код метода
    }

Просмотрите код MainActivity.java:

Просмотреть код MainActivity.java

package interosite.ru.bluetoothdemo;

import android.app.ListActivity;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends ListActivity {

    public final static String UUID = "e91521df-92b9-47bf-96d5-c52ee838f6f6";

    private class WriteTask extends AsyncTask<String, Void, Void> {
        protected Void doInBackground(String... args) {
            try {
                clientThread.getCommunicator().write(args[0]);
            } catch (Exception e) {
                Log.d("MainActivity", e.getClass().getSimpleName() + " " + e.getLocalizedMessage());
            }
            return null;
        }
    }

    private BluetoothAdapter bluetoothAdapter;
    private BroadcastReceiver discoverDevicesReceiver;
    private BroadcastReceiver discoveryFinishedReceiver;

    private final List<BluetoothDevice> discoveredDevices = new ArrayList<BluetoothDevice>();

    private ArrayAdapter<BluetoothDevice> listAdapter;

    private TextView textData;
    private EditText textMessage;

    private ProgressDialog progressDialog;

    private ServerThread serverThread;

    private ClientThread clientThread;

    private final CommunicatorService communicatorService = new CommunicatorService() {
        @Override
        public Communicator createCommunicatorThread(BluetoothSocket socket) {
            return new CommunicatorImpl(socket, new CommunicatorImpl.CommunicationListener() {
                @Override
                public void onMessage(final String message) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textData.setText(textData.getText().toString() + "\n" + message);
                        }
                    });
                }
            });
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        textData = (TextView) findViewById(R.id.data_text);
        textMessage = (EditText) findViewById(R.id.message_text);

        listAdapter = new ArrayAdapter<BluetoothDevice>(getBaseContext(), android.R.layout.simple_list_item_1, discoveredDevices) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View view = super.getView(position, convertView, parent);
                final BluetoothDevice device = getItem(position);
                ((TextView) view.findViewById(android.R.id.text1)).setText(device.getName());
                return view;
            }
        };
        setListAdapter(listAdapter);

    }

    public void makeDiscoverable(View view) {
        Intent i = new Intent(
                BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
        i.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
        startActivity(i);
    }

    public void discoverDevices(View view) {

        discoveredDevices.clear();
        listAdapter.notifyDataSetChanged();

        if (discoverDevicesReceiver == null) {
            discoverDevicesReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    String action = intent.getAction();

                    if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                        if (!discoveredDevices.contains(device)) {
                            discoveredDevices.add(device);
                            listAdapter.notifyDataSetChanged();
                        }
                    }
                }
            };
        }

        if (discoveryFinishedReceiver == null) {
            discoveryFinishedReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    getListView().setEnabled(true);
                    if (progressDialog != null)
                        progressDialog.dismiss();
                    Toast.makeText(getBaseContext(), "Поиск закончен. Выберите устройство для отправки ообщения.", Toast.LENGTH_LONG).show();
                    unregisterReceiver(discoveryFinishedReceiver);
                }
            };
        }

        registerReceiver(discoverDevicesReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
        registerReceiver(discoveryFinishedReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));

        getListView().setEnabled(false);

        progressDialog = ProgressDialog.show(this, "Поиск устройств", "Подождите...");

        bluetoothAdapter.startDiscovery();
    }

    @Override
    public void onPause() {
        super.onPause();
        bluetoothAdapter.cancelDiscovery();

        if (discoverDevicesReceiver != null) {
            try {
                unregisterReceiver(discoverDevicesReceiver);
            } catch (Exception e) {
                Log.d("MainActivity", "Не удалось отключить ресивер " + discoverDevicesReceiver);
            }
        }

        if (clientThread != null) {
            clientThread.cancel();
        }
        if (serverThread != null) serverThread.cancel();
    }

    @Override
    public void onResume() {
        super.onResume();
        serverThread = new ServerThread(communicatorService);
        serverThread.start();

        discoveredDevices.clear();
        listAdapter.notifyDataSetChanged();
    }

    public void onListItemClick(ListView parent, View v,
                                int position, long id) {
        if (clientThread != null) {
            clientThread.cancel();
        }

        BluetoothDevice deviceSelected = discoveredDevices.get(position);

        clientThread = new ClientThread(deviceSelected, communicatorService);
        clientThread.start();

        Toast.makeText(this, "Вы подключились к устройству \"" + discoveredDevices.get(position).getName() + "\"", Toast.LENGTH_SHORT).show();
    }

    public void sendMessage(View view) {
        if (clientThread != null) {
            new WriteTask().execute(textMessage.getText().toString());
            textMessage.setText("");
        } else {
            Toast.makeText(this, "Сначала выберите клиента", Toast.LENGTH_SHORT).show();
        }
    }

}

SDP-идентификатор

Как отмечено выше, Bluetooth SDP подразумевает использование идентификатора сервиса, которое предоставляет приложение.

В классе MainActivity определяем идентификатор следующим образом:

public final static String UUID = "e91521df-92b9-47bf-96d5-c52ee838f6f6";

Нужно определить UUID один раз для вашего приложения и применять его в неизменно виде. Чтобы сгенерировать UUID, можно воспользоваться одним из онлайн-сервисов (например, этим)

Реализация фабрики Communicator-ов

Для получения сообщений от входящих подключений предусмотрен класс ConnectorImpl, реализующий общий интерфейс Connector. Чтобы отложить процесс создания коннектора до нужного момента, коннектор создается не напрямую, а с помощью сервиса ServiceConnector. Далее, этот сервис передается («инжектируется»), в классы ClientThread и ServerThread (см. далее):

    private final CommunicatorService communicatorService = new CommunicatorService() {
        @Override
        public Communicator createCommunicatorThread(BluetoothSocket socket) {
            return new CommunicatorImpl(socket, new CommunicatorImpl.CommunicationListener() {
                @Override
                public void onMessage(final String message) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            textData.setText(textData.getText().toString() + "\n" + message);
                        }
                    });
                }
            });
        }
    };

Пока обратите внимание, что создается объект CommunicatorImpl, который принимает BluetoothSocket и специальный коллбек CommunicationListener. Последний служит для получения сообщений от потоков ClientThread и ServerThread.
В методе onMessage передается управление в главный поток приложения с помощью метода активности runOnUiThread. В главном потоке полученный текст добавляется к тексту поля textData (android:id=»@+id/data_text» в xml-ресурсе интерфейса пользователя).

Инициализация активности — метод onCreate

В методе жизненного цикла onCreate необходимо сделать первоначальную инициализацию переменных и пользовательского интерфейса:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        textData = (TextView) findViewById(R.id.data_text);
        textMessage = (EditText) findViewById(R.id.message_text);

        listAdapter = new ArrayAdapter<BluetoothDevice>(getBaseContext(), android.R.layout.simple_list_item_1, discoveredDevices) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                View view = super.getView(position, convertView, parent);
                final BluetoothDevice device = getItem(position);
                ((TextView) view.findViewById(android.R.id.text1)).setText(device.getName());
                return view;
            }
        };
        setListAdapter(listAdapter);

    }
  • BluetoothAdapter.getDefaultAdapter() — получаем bluetooth-адаптер устройства. С этого начинается любая работа с bluetooth в Android. В настоящее время Андроид поддерживает только один адаптер. В документации отмечено, что на версиях начиная JELLY_BEAN_MR2 для получения адаптера нужно пользоваться методом Context.getSystemService(BLUETOOTH_SERVICE). Однако сейчас никакой разницы между этими двумя способами, похоже, нет.
  • listAdapter — создаем адаптер для списка устройств. Данный адаптер получает данные из списка discoveredDevices подключенных устройств. Затем этот адаптер присваивается листу:

    setListAdapter(listAdapter).
    

    Метод setListAdapter является частью родительского класса ListActivity, от которого наследуется MainActivity.

Делаем устройство доступным для поиска по Bluetooth

Обработчик нажатия кнопки «Сделать доступным для поиска» выглядит так:

public void makeDiscoverable(View view) {
        Intent i = new Intent(
                BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
        i.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
        startActivity(i);
    }

Параметр EXTRA_DISCOVERABLE_DURATION задает время, на которое запрашивается состояние доступности для поиска.

Поиск устройств

При нажатии на кнопку «Найти устройства» происходит вызов метода discoverDevices:

    public void discoverDevices(View view) {

        discoveredDevices.clear();
        listAdapter.notifyDataSetChanged();

        if (discoverDevicesReceiver == null) {
            discoverDevicesReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    String action = intent.getAction();

                    if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                        if (!discoveredDevices.contains(device)) {
                            discoveredDevices.add(device);
                            listAdapter.notifyDataSetChanged();
                        }
                    }
                }
            };
        }

        if (discoveryFinishedReceiver == null) {
            discoveryFinishedReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    getListView().setEnabled(true);
                    if (progressDialog != null)
                        progressDialog.dismiss();
                    Toast.makeText(getBaseContext(), "Поиск закончен. Выберите устройство для отправки  cообщения.", Toast.LENGTH_LONG).show();
                    unregisterReceiver(discoveryFinishedReceiver);
                }
            };
        }

        registerReceiver(discoverDevicesReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
        registerReceiver(discoveryFinishedReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));

        getListView().setEnabled(false);

        progressDialog = ProgressDialog.show(this, "Поиск устройств", "Подождите...");

        bluetoothAdapter.startDiscovery();
    }

Тут регистрируются два ресивера:
discoverDevicesReceiver — принимает событие BluetoothDevice.ACTION_FOUND. В обработчике ресивера полученное устройство добавляется к списку discoveredDevices.
discoveryFinishedReceiver — вызывается системой, когда поиск устройств завершен.

Когда ресиверы зарегистрированы, запускается поиск:

bluetoothAdapter.startDiscovery();

Запуск и остановка потоков ClientThread и ServerThread

Для возобновления работы и остановки потоков в MainActivity используются методы жизненного цикла onResume и onPause. Первый вызывается, когда активность получает фокус и может взаимодействовать с пользователем. Второй — когда активность теряет фокус. Соответственно, onResume подходит для запуска потока, отвечающего за прием соединений от других устройств (Servet thread). В onPause наоборот, нужно остановить все потоки, освободив тем самым ресурсы:

Посмотреть код

    @Override
    public void onPause() {
        super.onPause();
        bluetoothAdapter.cancelDiscovery();

        if (discoverDevicesReceiver != null) {
            try {
                unregisterReceiver(discoverDevicesReceiver);
            } catch (Exception e) {
                Log.d("MainActivity", "Не удалось отключить ресивер " + discoverDevicesReceiver);
            }
        }

        if (clientThread != null) {
            clientThread.cancel();
        }
        if (serverThread != null) serverThread.cancel();
    }

    @Override
    public void onResume() {
        super.onResume();
        serverThread = new ServerThread(communicatorService);
        serverThread.start();

        discoveredDevices.clear();
        listAdapter.notifyDataSetChanged();
    }

Подключение у устройству и отправка сообщений

Два оставшихся метода метода служат для подключения в устройству bluetooth на Андроид.

Первый — onListItemClick. Этот метод вызывается в классе ListActivity при нажатии на элемент списка. Тут пересоздается поток подключения ClientThread, которому передается выбранное устройство:


    public void onListItemClick(ListView parent, View v,
                                int position, long id) {
        if (clientThread != null) {
            clientThread.cancel();
        }

        BluetoothDevice deviceSelected = discoveredDevices.get(position);

        clientThread = new ClientThread(deviceSelected, communicatorService);
        clientThread.start();

        Toast.makeText(this, "Вы подключились к устройству \"" + discoveredDevices.get(position).getName() + "\"", Toast.LENGTH_SHORT).show();
    }

Обработчик кнопки «Отправить сообщение» sendMessage запускает AsynTask, в котором происходит запись сообщения в исходящий поток:


    public void sendMessage(View view) {
        if (clientThread != null) {
            new WriteTask().execute(textMessage.getText().toString());
            textMessage.setText("");
        } else {
            Toast.makeText(this, "Сначала выберите клиента", Toast.LENGTH_SHORT).show();
        }
    }

Обратите внимание, что конструкторы ServerThread и ClientThread принимают communicatorService, который они используют для создания экземпляров коммуникатора. Как это происходит, вы сейчас узнаете.

Теперь все готово чтобы перейти непосредственно к процессу коммуникации по bluetooth.

Отправка сообщение по bluetooth — класс ClientThread

Класс ClientThread получает сокет у выбранного устройства для последующей записи в него. Просмотрите код класса:

Класс ClientThread

package interosite.ru.bluetoothdemo;

import java.io.IOException;
import java.util.UUID;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

public class ClientThread extends Thread {

    private volatile Communicator communicator;

    private final BluetoothSocket socket;
    private BluetoothAdapter bluetoothAdapter;
    private final CommunicatorService communicatorService;

    public ClientThread(BluetoothDevice device, CommunicatorService communicatorService) {

        this.communicatorService = communicatorService;

        BluetoothSocket tmp = null;
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        try {
            tmp = device.createRfcommSocketToServiceRecord(UUID.fromString(MainActivity.UUID));
        } catch (IOException e) {
            Log.d("ClientThread", e.getLocalizedMessage());
        }
        socket = tmp;
    }

    public synchronized Communicator getCommunicator() {
        return communicator;
    }

    public void run() {
        bluetoothAdapter.cancelDiscovery();
        try {
            Log.d("ClientThread", "About to connect");
            socket.connect();
            Log.d("ClientThread", "Connected");
            synchronized (this) {
                communicator = communicatorService.createCommunicatorThread(socket);
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Log.d("ClientThread", "Start");
                    communicator.startCommunication();
                }
            }).start();
        } catch (IOException connectException) {
            try {
                socket.close();
            } catch (IOException closeException) {
                Log.d("ClientThread", closeException.getLocalizedMessage());
            }
        }
    }

    public void cancel() {
        if (communicator != null) communicator.stopCommunication();
    }

}

В конструкторе получаем BluetoothSocket для исходящих соединений (подключения к другим устройствам):

tmp = device.createRfcommSocketToServiceRecord(UUID.fromString(MainActivity.UUID));

Метод createRfcommSocketToServiceRecord принимает UUID, который будет использоваться протоколом SDP для поиска сервиса с таким же UUID.

Основной метод потока run осуществляет подключение к сокету устройства (socket.connect()). Затем, управление передается коммуникатору:

    public void run() {
        bluetoothAdapter.cancelDiscovery();
        try {
            Log.d("ClientThread", "About to connect");
            socket.connect();
            Log.d("ClientThread", "Connected");
            synchronized (this) {
                communicator = communicatorService.createCommunicatorThread(socket);
            }
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Log.d("ClientThread", "Start");
                    communicator.startCommunication();
                }
            }).start();
        } catch (IOException connectException) {
            try {
                socket.close();
            } catch (IOException closeException) {
                Log.d("ClientThread", closeException.getLocalizedMessage());
            }
        }
    }

Обратите внимание, что в начале метода отключается поиск устройств для экономии ресурсов устройства:

bluetoothAdapter.cancelDiscovery();

Получение сообщений от других устройств — класс ServerThread

ServerThread принимает входящие соединения и читает данные из полученного сокета. Поток ServerThread запущен все время, пока MainActivity находится в активном состоянии (между вызовами onResume и onPause). Возможно, в вашем приложении вы захотите вынести ServerThread в сервис, так чтобы его работа не зависела от состояния пользовательского интерфейса (активности).

Просмотреть код
public class ServerThread extends Thread {
    
    private final BluetoothServerSocket bluetoothServerSocket;
    private final CommunicatorService communicatorService;

    public ServerThread(CommunicatorService communicatorService) {
        this.communicatorService = communicatorService;
        final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        BluetoothServerSocket tmp = null;
        try {
            tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord("BluetoothApp", UUID.fromString(MainActivity.UUID));
        } catch (IOException e) {
            Log.d("ServerThread", e.getLocalizedMessage());
        }
        bluetoothServerSocket = tmp;
    }

    public void run() {

        BluetoothSocket socket = null;

        Log.d("ServerThread", "Started");

        while (true) {
            try {
                socket = bluetoothServerSocket.accept();
            } catch (IOException e) {
                Log.d("ServerThread", "Stop: " + e.getLocalizedMessage());
                break;
            }
            if (socket != null) {
                communicatorService.createCommunicatorThread(socket).startCommunication();
            }
        }
    }

    public void cancel() {
        try {
            bluetoothServerSocket.close();
        } catch (IOException e) {
            Log.d("ServerThread", e.getLocalizedMessage());
        }
    }
}

В конструкторе с помощью метода listenUsingRfcommWithServiceRecord создается прослушивающий сокет — BluetoothServerSocket. Метод принимает UUID и имя сервиса. В нашем случае имя простое — «BluetoothApp».

Основной метод полученного BluetoothServerSocket — accept. Этот метод ждет (блокирует выполнение программы), до получения входящего подключения. Accept возвращает сокет, из которого затем можно считывать данные:

    public void run() {

        BluetoothSocket socket = null;

        Log.d("ServerThread", "Started");

        while (true) {
            try {
                socket = bluetoothServerSocket.accept();
            } catch (IOException e) {
                Log.d("ServerThread", "Stop: " + e.getLocalizedMessage());
                break;
            }
            if (socket != null) {
                communicatorService.createCommunicatorThread(socket).startCommunication();
            }
        }
    }

Как только необходимость в получении новых соединений отпадает, нужно закрыть серверный сокет:

    public void cancel() {
        try {
            bluetoothServerSocket.close();
        } catch (IOException e) {
            Log.d("ServerThread", e.getLocalizedMessage());
        }
    }

Чтение сообщений и вывод их в интерфейс пользователя — класс CommunicatorImpl

Просмотреть код
package interosite.ru.bluetoothdemo;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import android.bluetooth.BluetoothSocket;
import android.util.Log;

public class CommunicatorImpl extends Thread implements Communicator {

    interface CommunicationListener {
        void onMessage(String message);
    }

    private final BluetoothSocket socket;
    private final InputStream inputStream;
    private final OutputStream outputStream;
    private final CommunicationListener listener;

    public CommunicatorImpl(BluetoothSocket socket, CommunicationListener listener) {
        this.socket = socket;
        this.listener = listener;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) {
            Log.d("CommunicatorImpl", e.getLocalizedMessage());
        }
        inputStream = tmpIn;
        outputStream = tmpOut;
    }

    @Override
    public void startCommunication() {
        byte[] buffer = new byte[1024];

        int bytes;

        Log.d("CommunicatorImpl", "Run the communicator");

        while (true) {
            try {
                bytes = inputStream.read(buffer);
                Log.d("CommunicatorImpl", "Read " + bytes + " bytes");
                if (listener != null) {
                    listener.onMessage(new String(buffer).substring(0, bytes));
                }
            } catch (IOException e) {
                Log.d("CommunicatorImpl", e.getLocalizedMessage());
                break;
            }
        }
    }

    public void write(String message) {
        try {
            Log.d("CommunicatorImpl", "Write " + message);
            outputStream.write(message.getBytes());
        } catch (IOException e) {
            Log.d("CommunicatorImpl", e.getLocalizedMessage());
        }
    }

    @Override
    public void stopCommunication() {
        try {
            socket.close();
        } catch (IOException e) {
            Log.d("CommunicatorImpl", e.getLocalizedMessage());
        }
    }

}

Класс CommunicatorImpl получает входной и выходной потоки из переданного ему сокета:

 
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();

В методе startCommunication запускается бесконечный цикл, в котором происходит чтение из входящего потока и передача в коллбек listener, который необходимо передать в конструктор:

 
bytes = inputStream.read(buffer);
Log.d("CommunicatorImpl", "Read " + bytes + " bytes");
if (listener != null) {
    listener.onMessage(new String(buffer).substring(0, bytes));
}

Для записи в исходящий в есть метод write:

 
try {
    Log.d("CommunicatorImpl", "Write " + message);
    outputStream.write(message.getBytes());
} catch (IOException e) {
    Log.d("CommunicatorImpl", e.getLocalizedMessage());
}

Вы уже видели, как он используется в MainActivity:

 
    private class WriteTask extends AsyncTask<String, Void, Void> {
        protected Void doInBackground(String... args) {
            try {
                clientThread.getCommunicator().write(args[0]);
            } catch (Exception e) {
                Log.d("MainActivity", e.getClass().getSimpleName() + " " + e.getLocalizedMessage());
            }
            return null;
        }
    }

Заключение

Cсылка на скачивание исходников

В статье рассмотрели простой механизм взаимодействия устройств в помощью bluetooth по протоколу SDP.
Наверняка, у вас будут вопросы в процессе реализации вашего приложения. Прошу оставлять свои вопросы в комментариях.

Удачного программирования!

About the author / 

admin

  • Вячеслав

    Здравствуйте, очень интересная тема. Скачал исходники, посмотреть, как это работает, но там нигде не нашел кусок кода из раздела «Реализация фабрики Communicator-ов»

  • Сергей Левкович

    Здравствуйте! Скачал исходники, запустил, нашел устройство, подключился к нему, однако после отправки сообщения в логах выдает NullPointerException null, как я понимаю не выполняется clientThread.getCommunicator().write(args[0]); в классе WriteTask. Подскажите, в чем проблема?

  • Sergey Salnikov

    Объясните пожалуйста фразу «интерфейс фабрики для создания Communicator-ов » и откуда берется класс CommunicatorService?

  • Pingback: Рабочие ссылки по OBD и контроллерам подстветки | sshevchuk()

  • Andron Gen Andrej Pedash

    Здравствуйте. Пожалуйста, помогите мне =(. Я начинающий и не знаю ни английского языка ни java. Но усугубление обстоятельств в моём странном восприятии программирования: ничего не понимаю в используемых терминах и теории как-бы ни пытался разобраться. Проблема в том, что я могу выучиться только разбирая конкретные примеры (только таким способом когда то смог очень хорошо выучить Паскаль, сделал дипломную работу с обработкой матриц, делал коряво игры, научился отправлять-получать сигналы через COM порт, таким образом управлял самодельной вентиляцией, а ещё программно сделал графическое отображение сердцебиения во время сна, что бы знать сколько снов снилось ночью, что сейчас есть в браслетах Сяоми). Т.е. — я тупой, но перспективный =).

    Теперь о главном. Ваша программа ЕДИНСТВЕННАЯ (из найденных), которая делает всё, что нужно вплоть до отправки сообщения(!), но в android studio 1.5 (только этот еееели смог установить). отображается много ошибок и главные из которых — уже в самом начале:

    private ServerThread serverThread;
    private ClientThread clientThread;

    Где ServerThread и ClientThread ужасно краснючие.

    Ещё выделяются communicatorService, .cancel(); и .start();, но надеюсь они исчезнут после исправления первого недуга.

    Поверьте, до моего вопроса здесь я сделал всё возможное в плане самостоятельного решения.

    • Владислав Хабибуллин

      Тут проект для Eclipse, а не android studio, поэтому немного проблемы при сборке ошибки
      вот нормальный проект

      https://yadi.sk/d/jApjLPZX3GYakw

  • Andrew Efremov

    Добрый день!
    Я хочу вынести вынести ServerThread в сервис. У меня вопрос, как передать в сервис объект сommunicatorService?