안드로이드는 블루투스를 지원하여 스마트폰이 다른 블루투스 기기와 무선으로 통신할 수 있습니다. 통신은 Android Bluetooth API를 통해서 이루어지는데, 이를 사용해서 다음과 같은 작업을 수행할 수 있습니다.
이제 위와 같은 기능들을 차례대로 수행하며 안드로이드에서 블루투스 연동을 사용해보도록 하겠습니다.
위의 블루투스 기능들을 사용하기 위해서 기본적으로 선언해야 하는 권한이 있습니다. 예제에서 사용할 프로젝트의 Target API가 32인 관계로 Android 12버전을 목표로 권한을 설정해보도록 하겠습니다.
<!-- Android 11 버전까지를 위한 권한 -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- 기기 검색을 위한 권한(위치가 필요하지 않고 장비만 검색하기 위해 위치는 무시) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<!-- 페어링된 기기를 확인하기 위한 권한 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
기본적으로 블루투스를 사용하기 위해서는 기기에서 블루투스가 지원되는지 확인하고 지원된다면 활성화되어 있는지 확인해야 합니다. 블루투스가 활성화 되어 있는지 확인하기 위해서는 BluetoothManager
, BluetoothAdapter
두 클래스를 이용해야 합니다. 두 클래스는 다음과 같은 역할을 수행합니다.
BluetoothManager: BluetoothAdapter
객체를 획득할 때 사용하는 클래스로 전체적인 블루투스 관리를 수행합니다.
BluetoothAdapter: 모든 Bluetooth 상호작용의 진입점입니다. 이를 사용해 다른 Bluetooth 장치를 검색하고, 연결된(페어링된) 장치 목록을 쿼리하고, MAC 주소를 사용하여 Bluetooth 기기를 인스턴스화(BluetoothDevice
객체 생성) 할 수 있고, 다른 기기와 통신을 위해 Socket(BluetoothServerSocket
)을 생성할 수 있습니다.
class MainActivity: AppCompatActivity() {
private val bluetoothManager: BluetoothManager by lazy {
getSystemService(BluetoothManager::class.java)
}
private val bluetoothAdapter: BluetoothAdapter? by lazy {
bluetoothManager.adapter
}
private val activityResultLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
showMessage(this, "블루투스 활성화")
} else if (it.resultCode == RESULT_CANCELED) {
showMessage(this, "취소")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// BluetoothAdapter가 Null이라면 블루투스를 지원하지 않는 것이므로 종료
if(bluetoothAdapter == null) {
showMessage(this, "블루투스를 지원하지 않는 장비입니다.")
finish()
}
}
// 활성화 요청
fun setActivate() {
bluetoothAdapter?.let {
// 비활성화 상태라면
if (!it.isEnabled) {
// 활성화 요청
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activityResultLauncher.launch(intent)
} else { // 활성 상태라면
showMessage(this, "이미 활성화 되어 있습니다")
}
}
}
// 비활성화 요청
fun setDeActivate() {
bluetoothAdapter?.let {
// 비활성화 상태라면
if (!it.isEnabled) {
showMessage(this, "이미 비활성화 되어 있습니다")
// 활성 상태라면
} else {
// 블루투스 비활성화
it.disable()
showMessage(this, "블루투스를 비활성화 하였습니다")
}
}
}
}
BluetoothManager
는 getSystemService()
메서드를 통해서 획득할 수 있습니다.BluetoothAdapter
는 BluetoothManager.getAdapter()
메서드를 통해서 획득할 수 있습니다. 이때, 해당 객체가 Null 이라면 블루투스를 지원하지 않는 장비입니다.isEnabled
메서드를 사용하면 현재 스마트폰에서 블루투스가 활성화 상태인지 비활성화 상태인지 알 수 있습니다. Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
를 시스템에 요청해서 블루투스 활성화를 요청할 수 있고, 비활성화가 필요하다면 BluetoothAdapter.disalbe()
메서드를 사용해서 비활성화로 만들 수 있습니다.기기 검색과 페어링 모두 BluetoothAdapter
클래스를 사용해서 수행합니다. 기기 검색은 로컬 영역에서 Bluetooth 지원 장치를 검색하는 것으로 검색이 가능하면 해당 기기의 이름, 클래스 및 고유 MAC 주소와 같은 정보를 공유하여 검색 요청에 응답합니다. 페어링은 처음으로 기기와 연결될 때 이루어지는 것으로, 페어링이 되면 해당 기기에 대한 정보가 저장됩니다.
기기 검색과 페어링에 대한 자세한 내용은 아래에서 확인해보겠습니다.
BluetoothAdapter.getBoundedDevices()
메서드를 통해서 획득할 수 있습니다.// 페어링된 디바이스 검색
fun getPairedDevices() {
bluetoothAdapter?.let {
// 블루투스 활성화 상태라면
if (it.isEnabled) {
// ArrayAdapter clear
adapter.clear()
// 페어링된 기기 확인
val pairedDevices: Set<BluetoothDevice> = it.bondedDevices
// 페어링된 기기가 존재하는 경우
if (pairedDevices.isNotEmpty()) {
pairedDevices.forEach { device ->
// ArrayAdapter에 아이템 추가
adapter.add(Pair(device.name, device.address))
}
} else {
showMessage(this, "페어링된 기기가 없습니다.")
}
} else {
showMessage(this, "블루투스가 비활성화 되어 있습니다.")
}
}
}
BluetoothAdapter.startDiscovery()
메서드를 통해서 수행합니다. startDiscovery()
메서드는 비동기 프로세스이며 검색이 성공적으로 시작했는지 여부를 나타내는 Boolean 값을 즉시 반환합니다.BroadcastReceiver
를 사용해야 합니다. BroadcastReceiver
코드입니다.// 기기 검색
fun findDevice() {
bluetoothAdapter?.let {
// 블루투스가 활성화 상태라면
if (it.isEnabled) {
// 현재 검색중이라면
if (it.isDiscovering) {
// 검색 취소
it.cancelDiscovery()
showMessage(this, "기기검색이 중단되었습니다.")
return
}
// ArrayAdapter clear
adapter.clear()
// 검색시작
it.startDiscovery()
showMessage(this, "기기 검색을 시작하였습니다")
} else {
showMessage(this, "블루투스가 비활성화되어 있습니다")
}
}
}
private lateinit var broadcastReceiver: BroadcastReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 블루투스 기기 검색 브로드캐스트
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
when (intent?.action) {
BluetoothDevice.ACTION_FOUND -> {
// BluetoothDevice 객체 획득
val device =
intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
// 기기 이름
val deviceName = device?.name
// 기기 MAC 주소
val deviceHardwareAddress = device?.address
if (deviceName != null && deviceHardwareAddress != null) {
adapter.add(Pair(deviceName, deviceHardwareAddress))
}
}
}
}
}
}
// BroadcastReceiver 등록
registerReceiver(broadcastReceiver, intentFilter)
}
override fun onDestroy() {
// BroadcastReceiver 등록해제
unregisterReceiver(broadcastReceiver)
}
네트워크 통신을 하기 위해서는 HTTP 통신 또는 Socket 통신을 수행합니다. HTTP 통신의 경우 Request-Response 구조로 Response를 받으면 통신을 끊는 Connectionless 특성을 가집니다. 이와는 다르게 Socket의 경우 한번 연결이 이루어지면 한쪽에서 연결을 끊기 전까지는 연결을 계속해서 유지합니다.
블루투스의 경우도 무선 이어폰을 사용해보셨다면 한번에 어떤 방법을 사용하는지 이해하실 수 있습니다. 한번 연결을 수행하면 직접 연결을 끊거나 오류가 생겨서 연결이 끊어지기 전까지는 계속해서 연결이 유지됩니다. 즉, 블루투스는 Socket을 사용하여 연결을 수행하는데 이에 대해 자세히 알아보도록 하겠습니다.
BluetoothSocket
를 각각 다른 방법으로 획득합니다.BluetoothSocket
에 연결되었을 때, 서로가 연결되었다고 간주합니다.InputStream
과 OutputStream
을 사용해서 데이터를 주고받을 수 있습니다.BluetoothServerSocket
을 열어 서버의 역할을 수행해야 합니다.BluetoothSocket
를 제공해야 합니다.listenUsingRfcommWithServiceRecord(String, UUID)
메서드를 사용하면 BluetoothServerSocket
을 획득할 수 있습니다.accept()
메서드를 수행하면 연결 요청에 대한 수신 대기를 시작합니다. 주의할 점은 accept()
메서드는 blocking 메서드이므로 Main Thread에서 수행하면 안되고 작업 스레드를 생성하여 수행해야 합니다.BluetoothServerSocket
으로부터 BluetoothSocket
를 획득했고 더 이상의 기기와 연결을 수행하지 않으려면 BluetoothServerSocket.close()
메서드를 호출하여 추가 연결을 종료해야 합니다. 이래도 서버 소켓과 모든 리소스가 해제되지만 accept()가 반환한 연결된 BluetoothSocket은 닫히지 않습니다.UUID: Universally unique identifier의 약쟈로서, 정보 식별을 위하여 사용되는 식별자이다. 128-bit 숫자로 이루어져 있으며, xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx 형식으로 표현한다.
// 블루투스에서 서버의 역할을 수행하는 스레드
class AcceptThread(private val bluetoothAdapter: BluetoothAdapter): Thread() {
private lateinit var serverSocket: BluetoothServerSocket
companion object {
private const val TAG = "ACCEPT_THREAD"
private const val SOCKET_NAME = "server"
private val MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
}
init {
try {
// 서버 소켓
serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(SOCKET_NAME, MY_UUID)
} catch(e: Exception) {
setLog(TAG, e.message.toString())
}
}
override fun run() {
var socket: BluetoothSocket? = null
while(true) {
try {
// 클라이언트 소켓
socket = serverSocket.accept()
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
socket?.let {
/* 클라이언트 소켓과 관련된 작업..... */
// 더 이상 연결을 수행하지 않는다면 서버 소켓 종료(그래도 연결된 소켓은 작동)
serverSocket.close()
}
break
}
}
fun cancel() {
try {
serverSocket.close()
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
}
}
BluetoothDevice
객체를 획득해야 합니다.BluetoothDevice
객체를 획득했다 이를 사용해서 연결을 초기화하는 BluetoothSocket
객체를 획득해야 합니다. BluetoothSocket
객체를 획득하는 메서드는 BluetoothDevice.createRfcommSocketToServiceRecord(UUID)
입니다.BluetoothSocket.connect()
메서드를 호출하여 연결을 수행합니다. 주의할 점은 해당 메서드는 blocking 메서드이므로 메인 스레드가 아닌 작업 스레드에서 해당 메서드를 호출해야 합니다.@SuppressLint("MissingPermission")
class ConnectThread(
private val myUUID: UUID,
private val device: BluetoothDevice,
) : Thread() {
companion object {
private const val TAG = "CONNECT_THREAD"
}
// BluetoothDevice 로부터 BluetoothSocket 획득
private val connectSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
device.createRfcommSocketToServiceRecord(myUUID)
}
override fun run() {
try {
// 연결 수행
connectSocket?.connect()
connectSocket?.let {
val connectedThread = ConnectedThread(bluetoothSocket = it)
connectedThread.start()
}
} catch (e: IOException) { // 기기와의 연결이 실패할 경우 호출
connectSocket?.close()
throw Exception("연결 실패")
}
}
fun cancel() {
try {
connectSocket?.close()
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
}
}
// 디바이스에 연결
private fun connectDevice(deviceAddress: String) {
bluetoothAdapter?.let { adapter ->
// 기기 검색을 수행중이라면 취소
if (adapter.isDiscovering) {
adapter.cancelDiscovery()
}
// 서버의 역할을 수행 할 Device 획득
val device = adapter.getRemoteDevice(deviceAddress)
// UUID 선언
val uuid = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
try {
val thread = ConnectThread(uuid, device)
thread.run()
showMessage(this, "${device.name}과 연결되었습니다.")
} catch (e: Exception) { // 연결에 실패할 경우 호출됨
showMessage(this, "기기의 전원이 꺼져 있습니다. 기기를 확인해주세요.")
return
}
}
}
BluetoothSocket
을 사용해서 데이터를 주고받을 수 있습니다.BloetoothSocket
의 InputStream
은 데이터를 받을 때 사용하고, OutputStream
은 데이터를 전송할 때 사용합니다.private inner class ConnectedThread(private val bluetoothSocket: BluetoothSocket) : Thread() {
private lateinit var inputStream: InputStream
private lateinit var outputStream: OutputStream
init {
try {
// BluetoothSocket의 InputStream, OutputStream 초기화
inputStream = bluetoothSocket.inputStream
outputStream = bluetoothSocket.outputStream
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
}
override fun run() {
val buffer = ByteArray(1024)
var bytes: Int
while (true) {
try {
// 데이터 받기(읽기)
bytes = inputStream.read(buffer)
setLog(TAG, bytes.toString())
} catch (e: Exception) { // 기기와의 연결이 끊기면 호출
setLog(TAG, "기기와의 연결이 끊겼습니다.")
break
}
}
}
fun write(bytes: ByteArray) {
try {
// 데이터 전송
outputStream.write(bytes)
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
}
fun cancel() {
try {
bluetoothSocket.close()
} catch (e: IOException) {
setLog(TAG, e.message.toString())
}
}
}
BluetoothHeadSet
클래스를 제공합니다.BluetoothA2dp
클래스를 제공합니다.BluetoothHealth
, BluetoothHealCallback
및 BluetoothHealthAppConfiguration
클르스를 포함합니다. 이를 사용하면 블루투스를 사용해 블루투스를 지원하는 의료 기기(심박측정기, 혈압게, 체온계)와 통신하는 애플리케이션을 만들 수 있습니다.BluetoothManager.getDefaultAdapter()
메서드를 사용하여 BluetoothAdapter
객체를 획득합니다.BluetoothProfile.ServiceListener
인터페이스를 구현하는 클래스를 생성합니다. 이 리스너는 서비스에 연결되거나 연결이 끊어진 경우 BluetoothProfile
클라이언트에게 알려줍니다.getProfileProxy()
메서드를 사용하여 프로필에 연결된 프포필 프록시 객체에 대한 연결을 설정합니다.BluetoothProfile.ServiceListener.onServiceConnected()
메서드에서 프록시 객체를 처리합니다.프록시(Proxy):
대신
이라는 의미를 가지고 있습니다. 프로토콜에 있어서 대리 응답 등에서 사용하는 개념이라고 할 수 있습니다. 보안상의 문제로 직접 통신을 주고 받을 수 없는 사이에서 프록시를 이용해서 중계를 하는 개념이라고 볼 수 있습니다. 이렇게 중계의 기능을 하는 것 을 프록시 서버 라고 부릅니다.
참조
Bluetooth란 무엇인가?
블루투스 프로파일 개요
Android Developer - Bluetooth
Android-Bluetooth
neuronicle-Fx2
프록시란 무엇인가?
UUID란 무엇인가?
2022.08.31에 업데이트되었고 변동사항이 있을 경우 글을 업데이트 하도록 하겠습니다.
틀린 부분은 댓글로 남겨주시면 수정하겠습니다..!!
TEST를 해보고싶은데 전체 소스코드좀 받아볼수있을까요?