Платформа Android предоставляет разработчику богатые коммуникационные возможности. Для работы с Bluetooth в состав Android входит мощный API, позволяющий легко производить сканирование окружающего пространства на предмет наличия готовых к соединению устройств и передачу данных между устройствами. В этой статье подробно показано, как использовать Bluetooth технологию в своих приложениях. Листинги приведены как для Java, так и для Kotlin. Статья имеет следующую структуру:
Платформа Android предоставляет разработчику два основных стека API для работы с беспроводной связью:
В рамках этой статьи мы не будем углубляться в суть работы с медицинскими bluetooth устройствами и сосредоточимся на первых четырех классах, с помощью которых можно организовать передачу данных между двумя телефонами. В этом случае р абота с Bluetooth состоит из четырех этапов: установка настроек bluetooth адаптера, поиск доступных для соединения устройств, установка соединения, передача данных.

Если Вы решили задействовать в своей программе возможности Bluetooth модуля, вам необходимо, прежде всего, подключить соответствующий пакет API.
import android.bluetooth.*;
Помимо этого необходимо дать приложению необходимые разрешения. Прежде всего необходимо разрешить BLUETOOTH. Если Вы собираетесь использовать критические с точки зрения безопасности возможности, например, изменить имя устройства, то нужно дать более мощные разрешения BLUETOOTH_ADMIN. (При использовании разрешения BLUETOOTH_ADMIN, необходимо также указывать и BLUETOOTH.) Начиная с Android 6.0 (API 23), сканирование Bluetooth-устройств может быть использовано для сбора данных о местоположении пользователя. Поэтому система требует соответствующего разрешения ACCESS_FINE_LOCATION. С учетом всего вышесказанного, в файле манифеста должны быть следующие строки
<manifest> <!-- Разрешение на использование Bluetooth для связи --> <uses-permission android:name="android.permission.BLUETOOTH" /> <!-- Разрешение на управление настройками Bluetooth (включение, выключение, видимость) --> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- КРИТИЧЕСКИ ВАЖНО ДЛЯ ANDROID 6.0 (API 23) И ВЫШЕ --> <!-- Необходимо для поиска устройств через startDiscovery() --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Альтернатива, если точные координаты не требуются --> <!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> --> <!-- Для Android 12 (API 31) и выше, если приложение сканирует BLE-устройства, может также понадобиться BLUETOOTH_SCAN. В рамках классического Bluetooth это разрешение пока не требуется для startDiscovery(). --> </manifest>
Для Android 6.0 (API 23) и выше разрешение ACCESS_FINE_LOCATION необходимо запрашивать динамически в момент его необходимости (например, перед началом поиска устройств).
// Пример на Java public class BluetoothActivity extends AppCompatActivity { private static final int REQUEST_CODE_LOCATION_PERMISSION = 100; private void checkPermissionsAndStartDiscovery() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { // Разрешение не предоставлено if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { // Показываем объяснение пользователю showRationaleDialog(); } else { // Запрашиваем разрешение ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION_PERMISSION); } } else { // Разрешение уже предоставлено startBluetoothDiscovery(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_LOCATION_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { startBluetoothDiscovery(); } else { Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show(); } } } }
// Пример на Kotlin с использованием Activity Result API (рекомендуемый способ) import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat class BluetoothActivity : AppCompatActivity() { // Регистрируем контракт для запроса разрешения private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> if (isGranted) { // Разрешение предоставлено, можно начинать поиск startBluetoothDiscovery() } else { // Разрешение отклонено, объясняем пользователю необходимость Toast.makeText(this, "Без разрешения поиск устройств невозможен", Toast.LENGTH_LONG).show() } } private fun checkPermissionsAndStartDiscovery() { when { ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED -> { // Разрешение уже есть startBluetoothDiscovery() } shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> { // Пользователь уже отказывал, объясняем, зачем нужно разрешение showRationaleDialog() } else -> { // Запрашиваем разрешение впервые requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } } } // ... остальной код }
Прежде чем соединяться с кем-нибудь и передавать данные нужно убедиться, что ваш телефон имеет bluetooth модуль. Первым делом при работе с Bluetooth API нужно создать экземпляр класса BluetoothAdapter. Для его создания нужно вызвать метод getAdapter() экземпляра системной службы BluetoothManager. Фактически BluetoothAdapter представляет для нас точку входа для работы с Bluetooth на устройстве. Если телефон не поддерживает bluetooth, то конструктор возвращает значение "null". Нужно всегда проверять это условие, чтобы избежать ошибок, но даже если аппарат оснащен Bluetooth модулем, он может быть недоступен, поскольку пользователь просто отключил его. Для проверки доступности Bluetooth служит метод isEnabled(). В случае, если модуль отключен, можно предложить пользователю включить его. Соответствующий код приведен ниже
// Java //BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); //OLD VERSION BluetoothManager bluetoothManager = getSystemService(BluetoothManager.class); BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null) { // Устройство не поддерживает Bluetooth Toast.makeText(this, "Bluetooth не поддерживается", Toast.LENGTH_LONG).show(); return; } if (!bluetoothAdapter.isEnabled()) { // Запрашиваем включение Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
// Kotlin //val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() // OLD Version val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java) val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.getAdapter() if (bluetoothAdapter == null) { // Устройство не поддерживает Bluetooth Toast.makeText(this, "Bluetooth не поддерживается", Toast.LENGTH_LONG).show() return } // Проверяем, включен ли Bluetooth if (!bluetoothAdapter.isEnabled) { // Запрашиваем включение через системное диалоговое окно val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }
Если пользователь согласился на включение адаптера, в переменную enableBtIntent будет записано значение RESULT_OK. В противном случае - RESULT_CANCELED.
После того, как все проверки выполнены, можно приступать к работе. Давайте, например, отобразим имя и адрес нашего адаптера, вызвав методы getName() и getAddress().
// Java String status; if(bluetooth.isEnabled()){ String mydeviceaddress= bluetooth.getAddress(); String mydevicename= bluetooth.getName(); status= mydevicename+" : "+ mydeviceaddress; } else { status="Bluetooth выключен"; } Toast.makeText(this, status, Toast.LENGTH_LONG).show();
// Kotlin val status = if (bluetooth.isEnabled) { val myDeviceAddress = bluetooth.address val myDeviceName = bluetooth.name "$myDeviceName : $myDeviceAddress" } else { "Bluetooth выключен" } Toast.makeText(this, status, Toast.LENGTH_LONG).show()
Если приложение имеет разрешение BLUETOOTH_ADMIN, вы можете изменить имя Bluetooth устройства с помощью метода
bluetooth.setName("AndroidCoder");
для отображения состояния адаптера служит метод BluetoothAdapter.getState(). Этот метод может возвращать одно из следующих значений:
STATE_TURNING_ON
STATE_ON
STATE_TURNING_OFF
STATE_OFF
Часто в целях экономии заряда батареи Bluetooth выключен по умолчанию. Следующих код создает сообщение, в котором информирует пользователя о состоянии адаптера:
//Java String state= bluetooth.getState(); status= mydevicename+ ”: ”+ mydeviceaddress+" : "+ state;
//Kotlin val state = bluetooth.state status = "$mydevicename: $mydeviceaddress : $state"
Отследивать состояние Bluetooth адаптера можно прослушивая широковещательное намерение ACTION_STATE_CHANGED, которое система передает при каждом изменении состояния Bluetooth. Поля EXTRA_STATE и EXTRA_PREVIOUS_STATE содержат старое и новое состояние.
С помощью класса BluetoothAdapter, Вы можете найти расположенное поблизости bluetooth устройство, запустив сканирование или запросив список уже спаренных устройств.
При сканировании осуществляется поиск доступных bluetooth модулей вокруг вас. Если в поле досягаемости окажется устройство с разрешенным bluetooth, оно отправит в ответ на запрос некоторую информацию о себе: имя, класс, свой уникальный MAC адрес. На основе этой информации можно организовать соединение и передачу данных.
Сразу после установки соединения с удаленным устройством, пользователю будет автоматически показан запрос на соединение. В случае положительного ответа полученная информация (имя, класс и MAC адрес) сохраняется и может затем использоваться через bluetooth API. Так при следующем сеансе связи с данным устройством вам уже не придется проводить сканирование, поскольку необходимый MAC адрес уже будет занесен в базу вашего телефона и его можно просто выбрать из списка спаренных устройств.
Необходимо различать понятие спаренных и соединенных устройств. Спаренные устройства просто знают о существовании друг-друга, имеют ссылку-ключ, которую могут использовать для аутентификации, и способны создать шифрованное соединение друг с другом. Соединенные устройства разделяют один радиоканал и могут передавать данные друг другу. Текущая реализация bluetooth API требует, чтобы устройства были спарены перед соединением. (Спаривание выполняется автоматически, когда вы начинаете шифрованное соединение через Bluetooth API)
Прежде чем приступать к поиску устройств вокруг имеет смысл показать пользователю список уже известных системе устройств. Вполне возможно, что требуемый телефон окажется в этом списке. Множество (Set) BluetoothDevice содержит информацию об устройствах, с которыми уже происходило соединение.
// Java Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices(); for (BluetoothDevice device : pairedDevices) { String deviceName = device.getName(); String deviceAddress = device.getAddress(); // Добавляем deviceName и deviceAddress в список для отображения }
// Kotlin val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices pairedDevices?.forEach { device -> val deviceName = device.name ?: "Безымянное устройство" val deviceAddress = device.address // Добавляем deviceName и deviceAddress в список для отображения }
Для того чтобы инициализировать соединение нужно знать MAC адрес устройства, который содержится в deviceAddress.
Для того, чтобы начать сканирование радиодиапазона на предмет наличия доступных устройств просто вызовите метод startDiscovery(). Сканирование происходит в отдельном асинхронном потоке. Метод возвращает true, если запуск сканирования прошел успешно. Обычно процесс сканирования занимает порядка 10-15 секунд. Чтобы получить информацию о найденных устройствах Ваше приложение должно зарегистрировать BroadcastReceiver для интента ACTION_FOUND. Этот интент вызывается для каждого найденного устройства. Интент содержит дополнительные поля EXTRA_DEVICE и EXTRA_CLASS, которые содержат объекты BluetoothDevice и BluetoothClass соответственно.
@Override protected void onCreate(Bundle savedInstanceState) { ... // Регистрируем BroadcastReceiver для интента ACTION_FOUND. IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND); registerReceiver(receiver, filter); } // Создаем BroadcastReceiver для ACTION_FOUND. private final BroadcastReceiver receiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { // Поиск дал результат. Берем объект BluetoothDevice // и информацию из интента. BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); String deviceName = device.getName(); String deviceHardwareAddress = device.getAddress(); // MAC адрес } } }; @Override protected void onDestroy() { super.onDestroy(); ... // Освобождаем ACTION_FOUND receiver. unregisterReceiver(receiver); }
// Kotlin override fun onCreate(savedInstanceState: Bundle?) { ... // Регистрируем BroadcastReceiver для интента ACTION_FOUND. val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) registerReceiver(receiver, filter) } // Создаем BroadcastReceiver для ACTION_FOUND. private val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action: String = intent.action when(action) { BluetoothDevice.ACTION_FOUND -> { // Поиск дал результат. Берем объект BluetoothDevice // и информацию из интента. val device: BluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) val deviceName = device.name val deviceHardwareAddress = device.address // MAC адрес } } } } override fun onDestroy() { super.onDestroy() ... // Освобождаем ACTION_FOUND receiver. unregisterReceiver(receiver) }
Поиск Bluetooth устройств требует много ресурсов. Как только Вы нашли подходящее устройство, не забудьте остановить процесс сканирования. Это можно сделать с помощью метода cancelDiscovery(). Кроме того, если ваш телефон уже находится в соединении с каким-либо устройством, сканирование может значительно сузить ширину пропускания канала, поэтому лучше воздержаться от поиска новых устройств при установленном соединении.
Современные Android смартфоны не могут похвастаться долгим временем работы, поэтому все нормальные люди отключают Bluetooth модуль. Если Вы хотите дать своим пользователям возможность сделать телефон видимым для других телефонов, вызовите с помощью метода startActivityForResult(Intent, int) интент ACTION_REQUEST_DISCOVERABLE. В результате пользователю будет показано системное окно с запросом на перевод телефона в режим bluetooth видимости. По умолчанию этот режим включается на 120 секунд. Это время можно изменить с передав интенту дополнительный параметр EXTRA_DISCOVERABLE_DURATION. Максимально доступное время составляет 3600 секунд. Значение 0 переводит bluetooth модуль вашего телефона в режим постоянной видимости. Для примера создадим интент с запросом на переход в режим видимости на 300 секунд
//Java int requestCode = 1; Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivityForResult(discoverableIntent, requestCode);
// Kotlin val requestCode = 1; val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply { putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300) } startActivityForResult(discoverableIntent, requestCode)
В результате выполнения этого кода пользователю будет показан диалог с запросом. Если пользователь согласится, телефон будет переведен в режим видимости, и будет вызван callback метод onActivityResult() . В качестве результата методу будет передано число секунд, которое устройство будет видимым. Если пользователь откажется от предложения или произойдет ошибка, то интент вернет код RESULT_CANCELED. Перевод устройства в режим видимости автоматически включает bluetooth адаптер.
Если вы хотите получить уведомления, при изменении режима видимости Вашего устройства, зарегистрируйте BroadcastReceiver для интента ACTION_SCAN_MODE_CHANGED. Дополнительные поля EXTRA_SCAN_MODE и EXTRA_PREVIOUS_SCAN_MODE позволяют получить информацию о новом и старом состоянии соответственно. Они могут принимать значения SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE или SCAN_MODE_NONE. Первое значение указывает на то, что устройство доступно для поиска. Второе - устройство не доступно для поиска, но способно принимать соединения. Третье - не доступно для поиска и не может принимать соединения.
Вам не нужно переводить свой телефон в режим видимости, если вы инициализируете соединение. Видимым должно быть устройство к которому вы хотите подключиться.
Чтобы соединить два устройства, вы должны написать серверную и клиентскую часть кода. Одно из устройств должно открыть серверный сокет, а второе - инициализировать соединение, используя MAC адрес сервера. Сервер и клиент считаются соединенными, когда они оба имеют активный BluetoothSocket на одном и том же RFCOMM канале. После этого они могут поучать и отправлять потоки данных. Сервер и клиент по-разному получают требуемый BluetoothSocket. Сервер получает его, когда входящее соединение принято. Клиент - когда открывает RFCOMM для сервера.
При соединении устройств одно из них должно вести себя как сервер, то есть удерживать открытый BluetoothServerSocket. Цель сервера - ждать запроса на входящее соединение, и когда оно подтверждено, создать BluetoothSocket. После этого BluetoothServerSocket можно закрыть. Рассмотрим поэтапно процедуру соединения с точки зрения сервера:
Поскольку метод accept() является блокирующим, его не стоит вызывать из потока главной activity, поскольку это приведет к подвисанию интерфейса. Обычна вся работа с BluetoothServerSocket и BluetoothSocket выполняется в отдельном потоке. Чтобы прекратить выполнение метода accept(), вызовите метод close() для BluetoothServerSocket (или BluetoothSocket) из любого другого потока вашего приложения.
Ниже приведен пример потока, реализующий описанный выше механизм работы
//Java
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
// Используем вспомогательную переменную, которую в дальнейшем
// свяжем с mmServerSocket,
BluetoothServerSocket tmp = null;
try {
// MY_UUID это UUID нашего приложения, это же значение
// используется в клиентском приложении
tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket's listen() method failed", e);
}
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// ждем пока не произойдет ошибка или не
// будет возвращен сокет
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "Socket's accept() method failed", e);
break;
}
if (socket != null) {
// Соединение установлено. Выполните работу, связанную с
// соединением, в отдельном потоке.
manageMyConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
// Закрываем сокет и завершаем работу потока.
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Не могу закрыть сокет", e);
}
}
}
//Kotlin private inner class AcceptThread : Thread() { private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) { bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID) } override fun run() { // Слушаем пока не возникнет исключение или не будет возвращено значение сокета. var shouldLoop = true while (shouldLoop) { val socket: BluetoothSocket? = try { mmServerSocket?.accept() } catch (e: IOException) { Log.e(TAG, "Socket's accept() method failed", e) shouldLoop = false null } socket?.also { manageMyConnectedSocket(it) mmServerSocket?.close() shouldLoop = false } } } // Закрываем сокет и завершаем работу потока. fun cancel() { try { mmServerSocket?.close() } catch (e: IOException) { Log.e(TAG, "Не могу закрыть сокет", e) } } }
В этом примере подразумевается, что может быть установлено только одно соединение, поэтому после того, как соединение подтверждено и получен BluetoothSocket, приложение посылает его отдельному потоку, закрывает BluetoothServerSocket и выходит из цикла.
Обратите внимание, когда accept() возвращает BluetoothSocket, сокет уже соединен, поэтому не требуется вызывать метод connect().
manageConnectedSocket() представляет собой метод, внутри которого нужно создать поток для передачи данных. Его возможная реализация будет рассмотрена ниже.
Вы должны закрыть BluetoothServerSocket сразу же после завершения прослушивания эфира на предмет наличия входящего соединения. В приведенном примере метод close() вызывается сразу после получения объекта BluetoothSocket. Также Вам может понадобиться public метод для остановки приватного BluetoothSocket.
Для инициализации соединения с удаленным устройствам (устройством, которое держит открытым серверный сокет) вам необходимо получить объект BluetoothDevice, содержащий информацию о нем. Этот объект используется для получения BluetoothSocket и инициализации соединения.
Опишем процедуру соединения:
Как и в случае с accept, метод connect() следует выполнять в отдельном потоке, в противном случае может произойти подвисание интерфейса.
Замечание. Прежде чем вызывать connect() убедитесь, что в данный момент не происходит сканирование с целью поиска доступных устройств. В случае одновременного выполнения этих операций соединение будет устанавливаться намного медленнее, и вы рискуете не уложиться в timeout.
Приведем пример клиентского приложения, инициализирующего соединение
//Java private class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothDevice device) { // используем вспомогательную переменную, которую в дальнейшем // свяжем с mmSocket, BluetoothSocket tmp = null; mmDevice = device; try { // получаем BluetoothSocket чтобы соединиться с BluetoothDevice // MY_UUID это UUID, который используется и на сервере tmp = device.createRfcommSocketToServiceRecord(MY_UUID); } catch (IOException e) { Log.e(TAG, "Socket's create() method failed", e); } mmSocket = tmp; } public void run() { // Отменяем сканирование, поскольку оно тормозит соединение bluetoothAdapter.cancelDiscovery(); try { // Соединяемся с устройством через сокет. // Метод блокирует выполнение программы до // установки соединения или возникновения ошибки mmSocket.connect(); } catch (IOException connectException) { // Невозможно соединиться. Закрываем сокет и выходим. try { mmSocket.close(); } catch (IOException closeException) { Log.e(TAG, "Не могу закрыть клиентский сокет", closeException); } return; } // Попытка подключения прошла успешно. Выполните работу, // связанную с подключением, в отдельном потоке. manageMyConnectedSocket(mmSocket); } // Закрываем сокет и завершаем работу потока. public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "Не могу закрыть клиентский сокет", e); } } }
//Kotlin private inner class ConnectThread(device: BluetoothDevice) : Thread() { private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) { device.createRfcommSocketToServiceRecord(MY_UUID) } public override fun run() { // Отменяем сканирование, поскольку оно тормозит соединение bluetoothAdapter?.cancelDiscovery() mmSocket?.let { socket -> // Подключаемся к удаленному устройству через сокет. // Этот вызов блокирует поток до тех пор, пока не // завершится успешно или не будет выброшено исключение. socket.connect() // Попытка подключения прошла успешно. Выполните работу, // связанную с подключением, в отдельном потоке. manageMyConnectedSocket(socket) } } // Закрываем сокет и завершаем работу потока. fun cancel() { try { mmSocket?.close() } catch (e: IOException) { Log.e(TAG, "Could not close the client socket", e) } } }
Для остановки сканирования эфира вызывается метод cancelDiscovery(). Перед вызовом этого метода можно проверить идет ли сканирование с помощью isDiscovering().
После завершения работы с BluetoothSocket всегда вызывайте метод close(). Это поможет сэкономить ресурсы телефона.
После успешного соединения, каждое из соединенных устройств имеет объект BluetoothSocket с помощью которого легко реализовать передачу/прием данных:
Вы должны использовать отдельный поток для чтения и записи данных. Это важно, поскольку методы read(byte[]) и write(byte[]) являются блокирующими и их вызов в основном потоке может парализовать вашу программу. Главный цикл в этом отдельном потоке должен считывать данные из InputStream. Для записи в OutputStream имеет смысл создать отдельный public метод.
//Java public class MyBluetoothService { private static final String TAG = "MY_APP_DEBUG_TAG"; private Handler handler; // обработчик, получающий информацию от службы Bluetooth. // Определяет несколько констант, используемых при передаче сообщений между // сервисом и пользовательским интерфейсом. private interface MessageConstants { public static final int MESSAGE_READ = 0; public static final int MESSAGE_WRITE = 1; public static final int MESSAGE_TOAST = 2; // ... (При необходимости добавьте сюда другие типы сообщений.) } private class ConnectedThread extends Thread { private final BluetoothSocket mmSocket; private final InputStream mmInStream; private final OutputStream mmOutStream; private byte[] mmBuffer; // mmBuffer-хранилище для потока public ConnectedThread(BluetoothSocket socket) { mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; // Получаем входной и выходной потоки. Используем временные объекты, // так как поля класса для потоков объявлены как final. try { tmpIn = socket.getInputStream(); } catch (IOException e) { Log.e(TAG, "Произошла ошибка при создании входного потока.", e); } try { tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG, "Произошла ошибка при создании выходного потока.", e); } mmInStream = tmpIn; mmOutStream = tmpOut; } public void run() { mmBuffer = new byte[1024]; int numBytes; // байт возвращено в read() // Продолжайте прослушивать InputStream пока не возникнет исключение. while (true) { try { // Read from the InputStream. numBytes = mmInStream.read(mmBuffer); // Отправьте полученные байты в пользовательский интерфейс. Message readMsg = handler.obtainMessage( MessageConstants.MESSAGE_READ, numBytes, -1, mmBuffer); readMsg.sendToTarget(); } catch (IOException e) { Log.d(TAG, "Входной поток был разорван.", e); break; } } } // Вызовите этот код из основной activity, // чтобы отправить данные на удаленное устройство. public void write(byte[] bytes) { try { mmOutStream.write(bytes); // Передайте отправленное сообщение в пользовательский интерфейс. Message writtenMsg = handler.obtainMessage( MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer); writtenMsg.sendToTarget(); } catch (IOException e) { Log.e(TAG, "Ошибка при передаче данных", e); // Отправить сообщение об ошибке обратно в activity. Message writeErrorMsg = handler.obtainMessage(MessageConstants.MESSAGE_TOAST); Bundle bundle = new Bundle(); bundle.putString("toast", "Не могу отправить данные другому устройству"); writeErrorMsg.setData(bundle); handler.sendMessage(writeErrorMsg); } } // Вызовите этот код из основной activity для завершения соединения public void cancel() { try { mmSocket.close(); } catch (IOException e) { Log.e(TAG, "Не могу закрыть socket", e); } } } }
//Kotlin private const val TAG = "MY_APP_DEBUG_TAG" // Определяет несколько констант, используемых при передаче сообщений между // сервисом и пользовательским интерфейсом. const val MESSAGE_READ: Int = 0 const val MESSAGE_WRITE: Int = 1 const val MESSAGE_TOAST: Int = 2 // ... (При необходимости добавьте сюда другие типы сообщений.) class MyBluetoothService( // handler получает информацию от службы Bluetooth. private val handler: Handler) { private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() { private val mmInStream: InputStream = mmSocket.inputStream private val mmOutStream: OutputStream = mmSocket.outputStream private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer-хранилище для потока override fun run() { var numBytes: Int //байт возвращено в read() // Продолжайте прослушивать InputStream пока не возникнет исключение. while (true) { // Read from the InputStream. numBytes = try { mmInStream.read(mmBuffer) } catch (e: IOException) { Log.d(TAG, "Входной поток был разорван", e) break } // Отправьте полученные байты в UI activity. val readMsg = handler.obtainMessage( MESSAGE_READ, numBytes, -1, mmBuffer) readMsg.sendToTarget() } } // Вызовите этот код из основной activity, // чтобы отправить данные на удаленное устройство. fun write(bytes: ByteArray) { try { mmOutStream.write(bytes) } catch (e: IOException) { Log.e(TAG, "Ошибка при передаче данных", e) // Передайте отправленное сообщение в activity. val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST) val bundle = Bundle().apply { putString("toast", "Не могу отправить данные другому устройству") } writeErrorMsg.data = bundle handler.sendMessage(writeErrorMsg) return } // Поделиться отправленным сообщением с UI activity. val writtenMsg = handler.obtainMessage( MESSAGE_WRITE, -1, -1, mmBuffer) writtenMsg.sendToTarget() } // Вызовите этот код из основной activity для завершения соединения fun cancel() { try { mmSocket.close() } catch (e: IOException) { Log.e(TAG, "Не могу закрыть socket", e) } } } }
В конструкторе создаются объекты для работы с потоками данных, после чего поток оживает входящие данные. После того как прочитан очередной блок данных из входящего потока он посылается в главную activity посредствам вызова метода Handler родительского класса. Для отправки данных из главной activity просто вызывается метод write(). Внутри этого публичного метода происходит вызов write(byte[]). Метод close() также можно вызвать из главной activity. Он разрывает соединение.
Классический Bluetooth API остается мощным и необходимым инструментом для задач, требующих надежной потоковой передачи данных. Ключевые моменты для современных версий Android:
Эти основы позволят вам создавать приложения для обмена файлами, чаты, игры по Bluetooth и многое другое. Для работы с IoT, датчиками и устройствами с минимальным энергопотреблением изучите Bluetooth Low Energy (BLE).
Перевод:Александр Ледков
Источник: developer.android.com
Вы освоили работу с Bluetooth API. Следующий шаг к уровню Middle — научиться интегрировать такие решения в полный цикл разработки фичи, выбирать архитектуру и показывать свою экспертизу через портфолио.
Мы подготовили для вас целый раздел «Карьера в мобильной разработке», где вы найдёте:
Используйте знания системно — это ваш главный приоритет в 2026 году.