Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

블루투스 개요

Android 플랫폼은 블루투스 네트워크 스택에 대한 지원을 포함하므로 기기가 다른 블루투스 기기와 데이터를 무선으로 교환할 수 있습니다. 애플리케이션 프레임워크는 Android Bluetooth API를 통해 블루투스 기능에 대한 액세스 권한을 제공합니다. 애플리케이션은 이러한 API를 통해 다른 블루투스 기기에 무선으로 연결하여 지점 간 무선 기능 및 다중 지점 무선 기능을 활성화할 수 있습니다.

Android 애플리케이션은 Bluetooth API를 사용하여 다음과 같은 작업을 수행할 수 있습니다.

  • 다른 블루투스 기기 스캔
  • 페어링된 블루투스 기기에 대한 로컬 블루투스 어댑터 쿼리
  • RFCOMM 채널 설정
  • 서비스 검색을 통해 다른 기기에 연결
  • 기기 간 데이터 전송 및 수신
  • 다중 연결 관리

이 페이지에서는 클래식 블루투스를 중점으로 설명합니다. 클래식 블루투스는 Android 기기 간 스트리밍 및 통신과 같이 배터리를 많이 사용하는 작업에 적합한 옵션입니다. 전력 요구사항이 낮은 블루투스 기기를 위해 Android 4.3(API 레벨 18)부터는 저전력 블루투스용 API를 지원합니다. 자세한 내용은 저전력 블루투스를 참조하세요.

이 문서에서는 의료 기기 프로필을 포함한 여러 가지 블루투스 프로필을 설명합니다. 그런 다음 Android Bluetooth API를 사용하여 블루투스 통신에 필요한 4가지 주요 작업(블루투스 설정, 로컬 영역에서 페어링되었거나 사용 가능한 기기 찾기, 기기 연결, 기기 간 데이터 전송)을 수행하는 방법에 대해 설명합니다.

기본 사항

블루투스 지원 기기가 서로 데이터를 전송하려면 먼저 페어링 프로세스를 사용하여 통신 채널을 형성해야 합니다. 한쪽 기기, 즉 검색 가능한 기기가 수신되는 연결 요청 용도로 사용할 수 있게 됩니다. 다른 기기는 서비스 검색 프로세스를 사용해 검색 가능한 기기를 찾습니다. 검색 가능한 기기가 페어링 요청을 수락하면 두 기기가 보안 키를 교환하여 연결프로세스를 완료합니다. 이 키는 나중에 사용할 수 있도록 기기가 캐싱해둡니다. 두 기기는 페어링과 연결 프로세스를 완료한 뒤 정보를 교환합니다. 세션이 완료되면 페어링 요청을 시작한 기기가 자신을 검색 가능한 기기와 연결해준 채널을 해제합니다. 하지만 두 기기는 연결된 상태로 유지되므로 서로 범위 내에 있고 두 기기 중 어느 쪽도 연결을 삭제하지만 않았다면 향후 세션에서도 자동으로 다시 서로 연결할 수 있습니다.

블루투스 권한

애플리케이션에서 블루투스 기능을 사용하려면 두 개의 권한을 선언해야 합니다. 그중 첫 번째는 BLUETOOTH입니다. 이 권한은 연결 요청, 연결 수락 및 데이터 전송과 같은 블루투스 통신을 수행하는 데 필요합니다.

두 번째로 선언해야 하는 권한은 ACCESS_FINE_LOCATION입니다. 앱에 이 권한이 필요한 이유는 블루투스 스캔을 사용하여 사용자 위치에 대한 정보를 수집할 수 있기 때문입니다. 이 정보는 사용자 본인의 기기에서 가져올 수도 있고, 상점 및 교통 시설과 같은 위치에서 사용 중인 블루투스 비콘에서 가져올 수도 있습니다.

참고: 앱이 Android 9(API 레벨 28) 이하를 대상으로 하는 경우 ACCESS_FINE_LOCATION 권한 대신 ACCESS_COARSE_LOCATION 권한을 선언할 수 있습니다.

앱에서 기기 검색을 시작하거나 블루투스 설정을 조작하려면 BLUETOOTH 권한 외에 BLUETOOTH_ADMIN 권한도 선언해야 합니다. 대부분 애플리케이션은 로컬 블루투스 기기를 검색하는 기능에만 이 권한이 필요합니다. 애플리케이션이 사용자 요청 시 블루투스 설정을 수정하는 "파워 관리자"가 아닌 경우 이 권한이 부여하는 다른 기능은 사용해서는 안 됩니다.

애플리케이션 매니페스트 파일에 블루투스 권한을 선언합니다. 예를 들면 다음과 같습니다.

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

  <!-- If your app targets Android 9 or lower, you can declare
       ACCESS_COARSE_LOCATION instead. -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  ...
</manifest>

애플리케이션 권한 선언에 대한 자세한 내용은 <uses-permission> 참조 문서를 읽어보세요.

프로필 작업

Android 3.0부터 Bluetooth API는 Bluetooth 프로필 작업에 대한 지원을 포함합니다. Bluetooth 프로필은 기기 간 블루투스 기반 통신에 대한 무선 인터페이스 사양입니다. 일례로 Hands-Free 프로필이 있습니다. 휴대폰을 무선 헤드셋에 연결하려면 두 기기가 Hands-Free 프로필을 지원해야 합니다.

Android Bluetooth API는 다음 Bluetooth 프로필에 대한 구현을 제공합니다.

  • 헤드셋. Headset 프로필은 블루투스 헤드셋을 휴대폰과 함께 사용할 수 있도록 지원합니다. Android는 블루투스 헤드셋 서비스를 제어하는 프록시인 BluetoothHeadset 클래스를 제공합니다. 여기에는 블루투스 헤드셋 및 핸즈프리(v1.5) 프로필이 모두 포함됩니다. BluetoothHeadset 클래스에는 AT 명령에 대한 지원이 포함됩니다. 이 주제에 대한 자세한 내용은 업체 특정 AT 명령어를 참조하세요.
  • A2DP. A2DP(Advanced Audio Distribution Profile)는 블루투스 연결을 통해 고품질 오디오가 기기 간 스트리밍할 수 있는 방법을 정의합니다. Android는 블루투스 A2DP 서비스를 제어하는 프록시인 BluetoothA2dp 클래스를 제공합니다.
  • 의료 기기. Android 4.0(API 레벨 14)부터 HDP(블루투스 의료 기기 프로필)를 지원합니다. 이를 사용하면 블루투스를 사용하여 블루투스 지원 의료 기기(예: 심박측정기, 혈압계, 체온계, 체중계)와 통신하는 애플리케이션을 만들 수 있습니다. 지원되는 기기 목록 및 해당 기기 데이터 특수화 코드 목록은 블루투스의 HDP 기기 데이터 특수화를 참조하세요. 또한 이러한 값은 ISO/IEEE 11073-20601 [7] 사양에 Nomenclature Codes Annex의 MDC_DEV_SPEC_PROFILE_*로 참조됩니다. HDP에 대한 자세한 내용은 의료 기기 프로필을 참조하세요.

다음은 기본적인 프로필 작업 단계입니다.

  1. 블루투스 설정에서 설명한 기본 어댑터를 가져옵니다.
  2. BluetoothProfile.ServiceListener를 설정합니다. 이 리스너는 서비스에 연결되었거나 연결이 끊어진 경우 BluetoothProfile IPC 클라이언트에 알립니다.
  3. getProfileProxy()를 사용하여 프로필과 연결된 프로필 프록시 객체와의 연결을 설정합니다. 아래의 예시에서 프로필 프록시 객체는 BluetoothHeadset의 인스턴스입니다.
  4. onServiceConnected()에서 프로필 프록시 객체에 대한 핸들을 가져옵니다.
  5. 프로필 프록시 객체가 있는 경우 해당 객체를 사용하여 연결 상태를 모니터링하고 해당 프로필과 관련된 다른 작업을 수행할 수 있습니다.

예를 들어 다음 코드 스니펫은 헤드셋 프로필을 제어할 수 있도록 BluetoothHeadset 프록시 객체에 연결하는 방법을 보여줍니다.

Kotlin

var bluetoothHeadset: BluetoothHeadset? = null

// Get the default adapter
val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()

private val profileListener = object : BluetoothProfile.ServiceListener {

    override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = proxy as BluetoothHeadset
        }
    }

    override fun onServiceDisconnected(profile: Int) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null
        }
    }
}

// Establish connection to the proxy.
bluetoothAdapter?.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET)

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset)

Java

BluetoothHeadset bluetoothHeadset;

// Get the default adapter
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
    public void onServiceConnected(int profile, BluetoothProfile proxy) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = (BluetoothHeadset) proxy;
        }
    }
    public void onServiceDisconnected(int profile) {
        if (profile == BluetoothProfile.HEADSET) {
            bluetoothHeadset = null;
        }
    }
};

// Establish connection to the proxy.
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);

// ... call functions on bluetoothHeadset

// Close proxy connection after use.
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);

공급업체 특정 AT 명령어

Android 3.0(API 레벨 11)부터 애플리케이션은 헤드셋이 보낸 미리 정의된 공급업체 특정 AT 명령어(예: Plantronics +XEVENT 명령)의 시스템 브로드캐스트를 수신하도록 등록할 수 있습니다. 예를 들어 애플리케이션은 연결된 기기의 배터리 레벨을 나타내는 브로드캐스트를 수신할 수 있으며 사용자에게 알리거나 필요한 다른 조치를 취할 수 있습니다. ACTION_VENDOR_SPECIFIC_HEADSET_EVENT 인텐트가 헤드셋에 대한 공급업체 특정 AT 명령어를 처리하는 Broadcast Receiver를 생성합니다.

의료 기기 프로필

Android 4.0(API 레벨 14)부터 HDP(블루투스 의료 기기 프로필)를 지원합니다. 이를 사용하면 블루투스를 사용하여 블루투스를 지원하는 의료 기기(예: 심박측정기, 혈압계, 체온계, 체중계)와 통신하는 애플리케이션을 만들 수 있습니다. Bluetooth Health API는 BluetoothHealth, BluetoothHealthCallbackBluetoothHealthAppConfiguration 클래스를 포함하는데, 이에 대해서는 기본 사항에서 설명합니다.

Bluetooth Health API를 사용할 때 다음과 같은 핵심 HDP 개념을 이해하면 유용합니다.

소스
스마트 기기(예: Android 휴대폰이나 태블릿)에 의료 데이터를 전송하는 의료 기기(예: 체중계, 혈당계, 체온계)입니다.
싱크
의료 데이터를 수신하는 스마트 기기입니다. Android HDP 애플리케이션에서 싱크는 BluetoothHealthAppConfiguration 객체가 나타냅니다.
등록
특정 의료 기기와 통신하기 위해 싱크를 등록하는 데 사용되는 프로세스입니다.
연결
의료 기기(소스)와 스마트 기기(싱크) 사이의 채널을 여는 데 사용되는 프로세스입니다.

HDP 애플리케이션 만들기

다음은 Android HDP 애플리케이션을 만드는 기본 단계입니다.

  1. BluetoothHealth 프록시 객체에 대한 참조를 가져옵니다.

    일반 헤드셋 및 A2DP 프로필 기기와 마찬가지로 프로필 프록시 객체와 연결을 설정하도록 BluetoothProfile.ServiceListenerHEALTH 프로필 유형과 함께 getProfileProxy()를 호출해야 합니다.

  2. BluetoothHealthCallback을 만들고 의료 싱크로 작동하는 애플리케이션 구성(BluetoothHealthAppConfiguration)을 등록합니다.
  3. 의료 기기 연결을 설정합니다.

    참고: 연결을 자동으로 시작하는 기기도 있습니다. 해당 기기에 대해서는 이 단계를 수행할 필요가 없습니다.

  4. 의료 기기에 성공적으로 연결되었을 때 파일 설명자를 사용하여 의료 기기에 읽고 씁니다. 수신된 데이터는 IEEE 11073 사양을 구현하는 의료 관리자를 사용하여 해석해야 합니다.
  5. 완료되면 의료 채널을 닫고 애플리케이션 등록을 취소합니다. 확장된 비활성이 있는 경우에도 채널이 닫힙니다.

블루투스 설정

애플리케이션이 블루투스를 사용하여 통신하려면 블루투스가 기기에서 지원되는지 확인하고, 지원되는 경우 활성화해야 합니다.

블루투스가 지원되지 않는 경우 모든 블루투스 기능을 비활성화해야 합니다. 블루투스가 지원되지만 비활성화된 경우, 개발자는 사용자가 애플리케이션을 떠나지 않은 상태에서 블루투스를 활성화하도록 요청할 수 있습니다. 이 설정은 BluetoothAdapter를 사용하여 2단계로 수행됩니다.

  1. BluetoothAdapter를 가져옵니다.

    모든 블루투스 Activity에는 BluetoothAdapter가 필요합니다. BluetoothAdapter를 가져오려면 정적 getDefaultAdapter() 메서드를 호출합니다. 그러면 기기의 자체 블루투스 어댑터(블루투스 송수신 장치)를 나타내는 BluetoothAdapter가 반환됩니다. 전체 시스템에 대한 단일 블루투스 어댑터가 있고 애플리케이션이 해당 객체를 사용하여 상호작용할 수 있습니다. getDefaultAdapter()null을 반환하면 해당 기기는 블루투스를 지원하지 않는 것입니다. 예를 들면 다음과 같습니다.

    Kotlin

    val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    

    Java

    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
        // Device doesn't support Bluetooth
    }
    
  2. 블루투스를 활성화합니다.

    이제 블루투스를 활성화해야 합니다. isEnabled()를 호출하여 현재 블루투스가 활성화되었는지 확인합니다. 이 메서드가 false를 반환하는 경우 블루투스가 비활성화된 것입니다. 블루투스 활성화를 요청하려면 startActivityForResult()를 호출하고 ACTION_REQUEST_ENABLE 인텐트 작업에서 전달합니다. 이 호출은 시스템 설정을 통해 블루투스 활성화를 요청합니다(애플리케이션을 중단하지 않음). 예를 들면 다음과 같습니다.

    Kotlin

    if (bluetoothAdapter?.isEnabled == false) {
        val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
    }
    

    Java

    if (!bluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    

    그림 1과 같이 블루투스를 활성화하기 위해 사용자 권한을 요청하는 대화상자가 표시됩니다. 사용자가 "Yes"를 선택하면 시스템이 블루투스를 활성화하기 시작하고 해당 프로세스가 완료(또는 실패)되면 포커스가 애플리케이션으로 돌아갑니다.

    그림 1: 블루투스 활성화 대화상자.

    startActivityForResult()로 전달된 REQUEST_ENABLE_BT 상수는 로컬에서 정의된 정수로, 0보다 커야 합니다. 시스템은 onActivityResult() 구현에서 requestCode 매개변수로 이 상수를 다시 전달합니다.

    블루투스 활성화에 성공하면 Activity가 onActivityResult() 콜백에서 RESULT_OK 결과 코드를 수신합니다. 오류 때문에 블루투스를 활성화하지 못한 경우(또는 사용자가 "No"를 선택한 경우) 결과 코드는 RESULT_CANCELED입니다.

옵션으로 애플리케이션이 ACTION_STATE_CHANGED 브로드캐스트 인텐트를 수신 대기할 수도 있는데, 이것은 블루투스 상태가 변경될 때마다 시스템이 브로드캐스트합니다. 이 브로드캐스트는 새로운 블루투스 상태와 이전 블루투스 상태를 각각 포함하는 엑스트라 필드 EXTRA_STATEEXTRA_PREVIOUS_STATE를 포함합니다. 이 엑스트라 필드에 사용 가능한 값은 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFFSTATE_OFF입니다. 앱이 블루투스 상태에 적용된 런타임 변경사항을 감지해야 하는 경우 이 브로드캐스트를 수신 대기하는 것이 유용할 수 있습니다.

팁: 검색 기능을 활성화하면 블루투스가 자동으로 활성화됩니다. 블루투스 Activity를 수행하기 전에 기기 검색 기능을 일관되게 활성화할 계획인 경우, 위의 2단계를 건너뛰어도 됩니다. 자세한 내용은 이 페이지의 검색 기능 활성화 섹션을 참조하세요.

기기 찾기

BluetoothAdapter를 사용하면 기기 검색을 통해서 또는 페어링된 기기 목록을 쿼리하여 원격 블루투스 기기를 찾을 수 있습니다.

기기 검색은 주변 지역을 검색하여 블루투스 지원 기기가 있는지 찾아 각 기기에 관한 일부 정보를 요청하는 스캔 절차입니다. 이 프로세스를 검색, 조회 또는 스캔이라고도 합니다. 다만 근처에 있는 블루투스 기기가 검색 요청에 응답하려면 해당 기기가 검색 가능한 상태로 현재 정보 요청을 수락하고 있어야 합니다. 기기가 검색 가능한 경우 기기 이름, 클래스 및 고유 MAC 주소와 같은 정보를 공유하여 검색 요청에 응답합니다. 이때 이 정보를 사용하여 검색 프로세스를 실행하는 기기가 검색된 기기에 연결을 시작하도록 선택할 수 있습니다.

원격 기기와 처음으로 연결되면 페어링 요청이 자동으로 사용자에게 제공됩니다. 기기가 페어링되면 해당 기기에 대한 기본 정보(예: 기기 이름, 클래스 및 MAC 주소)는 저장되고 Bluetooth API를 사용하여 읽을 수 있습니다. 원격 기기에 대해 알려진 MAC 주소를 사용하면 (기기가 범위 내에 있다고 가정했을 때) 검색을 수행하지 않고 언제든지 연결을 시작할 수 있습니다.

페어링된 것과 연결된 것에는 차이점이 있습니다.

  • 페어링은 두 기기가 서로의 존재를 알고, 인증에 사용할 수 있는 공유 링크 키를 가지고 있으며, 서로 암호화된 연결을 설정할 수 있다는 것을 의미합니다.
  • 연결은 기기가 현재 RFCOMM 채널을 공유하고 있고 데이터를 서로 전송할 수 있다는 것을 의미합니다. 현재 Android Bluetooth API는 RFCOMM 연결을 설정할 수 있기 전에 기기를 페어링하도록 요청합니다. Bluetooth API와 암호화된 연결을 시작하면 페어링이 자동으로 실행됩니다.

아래 섹션에서는 페어링된 기기를 찾거나 기기 검색을 통해 새 기기를 검색하는 방법에 대해 설명합니다.

참고: Android 기기는 기본적으로 검색할 수 없습니다. 사용자가 시스템 설정을 통해 제한된 시간 동안 기기 검색이 가능하도록 설정하거나, 사용자가 애플리케이션을 떠나지 않고 검색 기능을 활성화하도록 애플리케이션에서 요청할 수 있습니다. 자세한 내용은 이 페이지의 검색 기능 활성화 섹션을 참조하세요.

페어링된 기기 쿼리

기기 검색을 수행하기 전에 페어링된 기기 집합을 쿼리하여 원하는 기기가 이미 있는지 확인하는 것이 좋습니다. 그렇게 하려면 getBondedDevices()를 호출합니다. 이렇게 하면 페어링된 기기를 나타내는 BluetoothDevice 객체 세트가 반환됩니다. 예를 들어 모든 페어링된 기기를 쿼리하여 각 기기의 이름과 MAC 주소를 가져올 수 있습니다. 이 내용을 다음 코드 스니펫으로 나타내었습니다.

Kotlin

val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter?.bondedDevices
pairedDevices?.forEach { device ->
    val deviceName = device.name
    val deviceHardwareAddress = device.address // MAC address
}

Java

Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();

if (pairedDevices.size() > 0) {
    // There are paired devices. Get the name and address of each paired device.
    for (BluetoothDevice device : pairedDevices) {
        String deviceName = device.getName();
        String deviceHardwareAddress = device.getAddress(); // MAC address
    }
}

블루투스 기기와 연결을 시작하려면 연결된 BluetoothDevice 객체에서는 MAC 주소만 있으면 됩니다. 이 주소는 getAddress()를 호출하여 검색합니다. 연결 생성에 대한 자세한 내용은 기기 연결 섹션을 참조하세요.

주의: 기기 검색을 수행하면 블루투스 어댑터의 리소스가 많이 소모됩니다. 연결할 기기를 검색한 뒤에는 연결을 시도하기 전에 cancelDiscovery()를 사용하여 검색을 중단했는지 확인합니다. 또한 기기에 연결된 상태로 검색을 수행하면 안 됩니다. 검색 프로세스가 기존 연결에서 사용 가능한 대역폭을 대폭 감소시키기 때문입니다.

기기 검색

기기 검색을 시작하려면 startDiscovery()를 호출합니다. 이 프로세스는 비동기식이고 검색이 성공적으로 시작되었는지 나타내는 부울 값을 반환합니다. 일반적으로 검색 프로세스는 12초 정도의 조회 스캔과, 검색된 각 기기의 페이지 스캔을 통해 블루투스 이름을 가져오는 과정이 포함됩니다.

애플리케이션이 검색된 각 기기에 대한 정보를 수신하려면 ACTION_FOUND 인텐트에 대한 BroadcastReceiver를 등록해야 합니다. 시스템이 각 기기에 대하여 이 인텐트를 브로드캐스트합니다. 인텐트에는 엑스트라 필드 EXTRA_DEVICEEXTRA_CLASS가 포함되고, 이 엑스트라 필드에는 각각 BluetoothDeviceBluetoothClass가 포함됩니다. 다음 코드 스니펫은 기기를 검색할 때 브로드캐스트를 처리하도록 등록하는 방법을 보여줍니다.

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Register for broadcasts when a device is discovered.
    val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
    registerReceiver(receiver, filter)
}

// Create a BroadcastReceiver for ACTION_FOUND.
private val receiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val action: String = intent.action
        when(action) {
            BluetoothDevice.ACTION_FOUND -> {
                // Discovery has found a device. Get the BluetoothDevice
                // object and its info from the Intent.
                val device: BluetoothDevice =
                        intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                val deviceName = device.name
                val deviceHardwareAddress = device.address // MAC address
            }
        }
    }
}

override fun onDestroy() {
    super.onDestroy()
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver)
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Register for broadcasts when a device is discovered.
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(receiver, filter);
}

// Create a BroadcastReceiver for 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)) {
            // Discovery has found a device. Get the BluetoothDevice
            // object and its info from the Intent.
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            String deviceName = device.getName();
            String deviceHardwareAddress = device.getAddress(); // MAC address
        }
    }
};

@Override
protected void onDestroy() {
    super.onDestroy();
    ...

    // Don't forget to unregister the ACTION_FOUND receiver.
    unregisterReceiver(receiver);
}

블루투스 기기와 연결을 시작하려면 연결된 BluetoothDevice 객체에서는 MAC 주소만 있으면 됩니다. 이 주소는 getAddress()를 호출하여 검색합니다. 연결 생성에 대한 자세한 내용은 기기 연결 섹션을 참조하세요.

주의: 기기 검색을 수행하면 블루투스 어댑터의 리소스가 많이 소모됩니다. 연결할 기기를 검색한 뒤에는 연결을 시도하기 전에 cancelDiscovery()를 사용하여 검색을 중단했는지 확인합니다. 또한 기기에 연결된 상태로 검색을 수행하면 안 됩니다. 검색 프로세스가 기존 연결에서 사용 가능한 대역폭을 대폭 감소시키기 때문입니다.

검색 기능 활성화

로컬 기기를 다른 기기가 검색할 수 있게 하려면 ACTION_REQUEST_DISCOVERABLE 인텐트를 사용하여 startActivityForResult(Intent, int)를 호출합니다. 이렇게 하면 설정 앱으로 이동할 필요 없이 시스템의 검색 가능한 모드를 활성화하도록 요청합니다. 설정 앱으로 이동하면 앱이 중단됩니다. 기본적으로 120초, 즉 2분간 기기가 검색 가능한 상태가 됩니다. 최대 3,600초(1시간)까지 다른 기간을 정의할 수도 있습니다. EXTRA_DISCOVERABLE_DURATION 엑스트라를 더하면 됩니다.

주의: EXTRA_DISCOVERABLE_DURATION 엑스트라의 값을 0으로 설정하면 해당 기기는 항상 검색 가능한 상태가 됩니다. 이 구성은 보안이 유지되지 않으므로 사용하지 않는 것이 좋습니다.

다음 코드 스니펫은 기기를 5분(300초)간 검색할 수 있게 설정한 것입니다.

Kotlin

val discoverableIntent: Intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
    putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300)
}
startActivity(discoverableIntent)

Java

Intent discoverableIntent =
        new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
그림 2: 검색 기능 활성화 대화상자.

그림 2와 같이 기기를 검색 가능하게 하는 사용자 권한을 요청하는 대화상자가 표시됩니다. 사용자가 "Yes"를 선택하면 기기는 지정된 시간 동안 검색 가능하게 됩니다. 그러면 Activity가 onActivityResult() 콜백에 대한 호출을 수신하고, 결과 코드는 기기가 검색 가능한 기간과 동일합니다. 사용자가 "No"를 선택하거나 오류가 발생한 경우 결과 코드는 RESULT_CANCELED입니다.

참고: 블루투스가 기기에서 활성화되지 않았을 경우 기기를 검색 가능하게 설정하면 블루투스가 자동으로 활성화됩니다.

기기는 할당된 시간 동안 자동으로 검색 가능한 모드로 유지됩니다. 검색 가능 모드가 변경되었을 때 알림을 받으려면 ACTION_SCAN_MODE_CHANGED 인텐트에 대해 BroadcastReceiver를 등록할 수 있습니다. 이 인텐트에는 엑스트라 필드 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE가 포함되어 있는데, 각각 새로운 스캔 모드와 이전 스캔 모드를 제공합니다. 각 엑스트라에 사용 가능한 값은 다음과 같습니다.

SCAN_MODE_CONNECTABLE_DISCOVERABLE
기기가 검색 가능 모드입니다.
SCAN_MODE_CONNECTABLE
기기가 검색 가능 모드가 아니지만 연결을 수신할 수는 있습니다.
SCAN_MODE_NONE
기기가 검색 가능 모드가 아니고 연결을 수신할 수 없습니다.

원격 기기로의 연결을 시작하는 경우, 기기 검색 가능 모드를 활성화하지 않아도 됩니다. 검색 가능 모드는 애플리케이션이 수신되는 연결을 수락하는 서버 소켓을 호스팅하도록 하고자 하는 경우에만 활성화하면 됩니다. 원격 기기는 다른 기기로 연결을 시작하기 전에 다른 기기를 검색할 수 있어야 하기 때문입니다.

기기 연결

두 기기 간 연결을 생성하려면 서버측 메커니즘과 클라이언트측 메커니즘을 모두 구현해야 합니다. 왜냐하면 한 기기는 서버 소켓을 열고 다른 기기는 서버 기기의 MAC 주소를 사용하여 연결을 시작해야 하기 때문입니다. 서버 기기와 클라이언트 기기는 각각 필요한 BluetoothSocket을 서로 다른 방식으로 획득합니다. 서버는 수신되는 연결을 수락할 때 소켓 정보를 받습니다. 클라이언트가 서버에 대한 RFCOMM 채널을 열 때 소켓 정보를 제공합니다.

서버와 클라이언트는 각각 동일한 RFCOMM 채널에 연결된 BluetoothSocket이 있는 경우 서로 연결된 것으로 간주됩니다. 이 시점에서 각 기기가 입력 및 출력 스트림을 획득할 수 있고 데이터 전송을 시작할 수 있는데, 이에 대해서는 연결 관리 섹션에서 설명합니다. 이 섹션에서는 두 기기 간 연결을 시작하는 방법에 대해 설명합니다.

연결 기술

각 기기가 서버 소켓을 열고 연결을 수신 대기하도록 각 기기를 서버로 자동으로 준비하는 구현 기술이 있습니다. 이 경우, 양쪽 기기 모두 다른 기기와 연결을 시작하고 클라이언트가 될 수 있습니다. 아니면 한 기기는 연결을 명시적으로 "호스팅"하고 요청 시 서버 소켓을 열 수 있게 되고, 다른 기기는 연결을 시작합니다.

그림 3: 블루투스 페어링 대화상자.

참고: 이전에 두 기기가 페어링되지 않은 경우 그림 3과 같이 Android 프레임워크가 연결 프로세스에서 사용자에게 페어링 요청 알림 또는 대화상자를 자동으로 표시합니다. 따라서 기기 연결을 시도할 때 애플리케이션은 기기 페어링 여부에 대해 걱정할 필요가 없습니다. RFCOMM 연결 시도는 사용자가 두 기기를 성공적으로 페어링할 때까지 차단되고, 사용자가 페어링을 거절하거나 페어링 프로세스가 실패하거나 시간 초과되면 연결 시도는 실패합니다.

서버로 연결

두 기기를 연결하려면 한 기기는 열린 BluetoothServerSocket을 제공하여 서버로 작동해야 합니다. 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 수락 시 연결된 BluetoothSocket을 제공하는 것입니다. BluetoothServerSocket에서 BluetoothSocket을 가져올 경우 추가 연결을 수락하지 않으려면 BluetoothServerSocket을 취소할 수 있으며 취소해야 합니다.

서버 소켓을 설정하고 연결을 수락하려면 다음 순서의 절차를 따르세요.

  1. listenUsingRfcommWithServiceRecord()를 호출하여 BluetoothServerSocket을 가져옵니다.

    이 문자열은 식별 가능한 서비스 이름이며 시스템이 이를 기기의 새 SDP(Service Discovery Protocol) 데이터베이스 항목에 자동으로 기록합니다. 이름은 임의로 지정하면 되고 단순히 애플리케이션 이름이어도 됩니다. UUID도 SDP 항목에 포함되며 이것이 클라이언트 기기와의 연결 동의를 판단할 기준이 됩니다. 다시 말해, 클라이언트가 이 기기와 연결을 시도할 때는 해당 클라이언트가 연결하고자 하는 서비스를 고유하게 식별하는 UUID를 제공합니다. 이 UUID가 일치해야 연결이 수락됩니다.

    UUID는 정보를 고유하게 식별하는 데 사용되는 표준화된 128비트 문자열 ID 형식입니다. UUID는 아무것이나 임의로 선택할 수 있을 만큼 충분히 크고 다른 ID와 충돌하지 않습니다. 이 경우 UUID는 애플리케이션의 블루투스 서비스를 고유하게 식별하는 데 사용됩니다. UUID를 애플리케이션과 함께 사용하려면 웹에서 수많은 임의의 UUID 생성기 중 하나를 사용한 다음 fromString(String)을 사용하여 UUID를 초기화할 수 있습니다.

  2. accept()를 호출하여 연결 요청에 대한 수신 대기를 시작합니다.

    이는 차단 호출이며, 연결이 수락되거나 예외가 발생한 경우 반환됩니다. 원격 기기가 이 수신 대기 서버 소켓에 등록된 것과 일치하는 UUID를 사용하여 연결 요청을 보내는 경우에만 연결이 수락됩니다. 연결이 성공하면 accept()가 연결된 BluetoothSocket을 반환합니다.

  3. 추가 연결을 수락하지 않으려면 close()를 호출합니다.

    그러면 이 메서드 호출이 서버 소켓과 모든 리소스를 해제하지만 accept()가 반환한 연결된 BluetoothSocket이 닫히지 않습니다. TCP/IP와 달리 RFCOMM은 한 번에 채널당 단일 연결 클라이언트만 허용하므로 대부분의 경우에 연결된 소켓을 수락한 직후에 BluetoothServerSocket에서 close()를 호출하는 것이 합당합니다.

accept() 호출은 차단 호출이므로 기본 Activity UI 스레드에서 실행되어서는 안 됩니다. 그래야 애플리케이션이 다른 사용자 상호작용에 반응할 수 있습니다. 일반적으로 애플리케이션이 관리하는 새 스레드에서 BluetoothServerSocket 또는 BluetoothSocket을 사용하여 모든 작업을 수행하는 것이 합당합니다. accept()와 같은 차단된 호출을 중단하려면 다른 스레드의 BluetoothServerSocket(또는 BluetoothSocket)에서 close()를 호출합니다. BluetoothServerSocket 또는 BluetoothSocket의 모든 메서드는 스레드로부터 안전합니다.

예시

다음은 들어오는 연결을 수락하는 서버 구성요소에 대한 간단한 스레드입니다.

Kotlin

private inner class AcceptThread : Thread() {
    
    private val mmServerSocket: BluetoothServerSocket? by lazy(LazyThreadSafetyMode.NONE) {
        bluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(NAME, MY_UUID)
    }

    override fun run() {
        // Keep listening until exception occurs or a socket is returned.
        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
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    fun cancel() {
        try {
            mmServerSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the connect socket", e)
        }
    }
}

Java

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket
        // because mmServerSocket is final.
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code.
            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;
        // Keep listening until exception occurs or a socket is returned.
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                Log.e(TAG, "Socket's accept() method failed", e);
                break;
            }

            if (socket != null) {
                // A connection was accepted. Perform work associated with
                // the connection in a separate thread.
                manageMyConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    // Closes the connect socket and causes the thread to finish.
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the connect socket", e);
        }
    }
}

이 예에서 들어오는 연결은 하나만 있는 것이 바람직합니다. 애플리케이션은 연결이 수락되고 BluetoothSocket을 가져오자마자 가져온 BluetoothSocket을 개별 스레드로 보내고, BluetoothServerSocket을 닫아서 해당 루프를 중단합니다.

accept()BluetoothSocket을 반환하면 소켓이 이미 연결된 것입니다. 그러므로 클라이언트측에서처럼 connect()를 호출해서는 안 됩니다.

각 앱의 manageMyConnectedSocket() 메서드는 데이터 전송을 위한 스레드를 시작하도록 설계되어 있습니다. 이 내용은 연결 관리 섹션에서 설명합니다.

일반적으로 들어오는 연결에 대한 수신을 완료하자마자 BluetoothServerSocket을 닫아야 합니다. 이 예시에서는 BluetoothSocket을 가져오자마자 close()를 호출했습니다. 서버 소켓에서 수신을 중지할 필요가 있는 경우 전용 BluetoothSocket을 닫을 수 있는 공용 메서드를 스레드에서 제공할 수도 있습니다.

클라이언트로 연결

열린 서버 소켓에서 연결을 수락하는 원격 기기와의 연결을 시작하려면 먼저 원격 기기를 나타내는 BluetoothDevice 객체를 얻어야 합니다. BluetoothDevice를 생성하는 방법에 대한 자세한 내용은 기기 찾기를 참조하세요. 그런 다음 BluetoothDevice를 사용하여 BluetoothSocket을 가져오고 연결을 시작해야 합니다.

기본 과정은 다음과 같습니다.

  1. BluetoothDevice를 통해 createRfcommSocketToServiceRecord(UUID)를 호출하여 BluetoothSocket을 가져옵니다.

    이 메서드는 BluetoothSocket 객체를 초기화하는데, 이 객체는 클라이언트가 BluetoothDevice에 연결하도록 허용합니다. 여기에 전달된 UUID는 서버 기기가 listenUsingRfcommWithServiceRecord(String, UUID)를 호출하여 BluetoothServerSocket을 열 때 사용한 UUID와 일치해야 합니다. 일치하는 UUID를 사용하려면 UUID 문자열을 애플리케이션에 하드코딩한 다음, 이를 서버와 클라이언트 코드 양쪽에서 모두 참조합니다.

  2. connect()를 호출하여 연결을 시작합니다. 이 메서드는 차단 호출입니다.

    클라이언트가 이 메서드를 호출한 뒤, 시스템에 SDP 조회를 실행하여 이와 일치하는 UUID를 포함한 원격 기기를 찾습니다. 이 조회가 성공적이고 원격 기기가 연결을 수락하는 경우 원격 기기가 연결하는 동안 사용할 RFCOMM 채널을 공유하고 connect()가 반환됩니다. 연결에 실패하거나 connect() 메서드의 시간이 초과되면(약 12초 후) 메서드가 IOException을 발생시킵니다.

    connect()가 차단 호출이므로, 이 연결 절차는 항상 기본 Activity(UI) 스레드와 분리된 스레드에서 수행해야 합니다.

    참고: connect()를 호출하기 전에 항상 cancelDiscovery()를 호출하여 기기가 기기 검색을 수행하고 있지 않은지 확인해야 합니다. 검색이 진행 중인 경우에는 연결 시도가 현저히 느려지고 실패할 가능성이 높습니다.

예시

블루투스 연결을 시작하는 클라이언트 스레드의 기본적인 예는 다음과 같습니다.

Kotlin

private inner class ConnectThread(device: BluetoothDevice) : Thread() {

    private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
        device.createRfcommSocketToServiceRecord(MY_UUID)
    }

    public override fun run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter?.cancelDiscovery()

        mmSocket?.use { socket ->
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            socket.connect()

            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
        }
    }

    // Closes the client socket and causes the thread to finish.
    fun cancel() {
        try {
            mmSocket?.close()
        } catch (e: IOException) {
            Log.e(TAG, "Could not close the client socket", e)
        }
    }
}

Java

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        // Use a temporary object that is later assigned to mmSocket
        // because mmSocket is final.
        BluetoothSocket tmp = null;
        mmDevice = device;

        try {
            // Get a BluetoothSocket to connect with the given BluetoothDevice.
            // MY_UUID is the app's UUID string, also used in the server code.
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) {
            Log.e(TAG, "Socket's create() method failed", e);
        }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it otherwise slows down the connection.
        bluetoothAdapter.cancelDiscovery();

        try {
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            mmSocket.connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and return.
            try {
                mmSocket.close();
            } catch (IOException closeException) {
                Log.e(TAG, "Could not close the client socket", closeException);
            }
            return;
        }

        // The connection attempt succeeded. Perform work associated with
        // the connection in a separate thread.
        manageMyConnectedSocket(mmSocket);
    }

    // Closes the client socket and causes the thread to finish.
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Could not close the client socket", e);
        }
    }
}

이 스니펫에서 cancelDiscovery()는 연결 시도 전에 호출됩니다. connect() 전에는 항상 cancelDiscovery()를 호출해야 합니다. 특히 cancelDiscovery()는 기기 검색이 현재 진행 중이든 아니든 관계없이 성공하기 때문입니다. 다만 앱이 기기 검색이 진행 중인지 판단해야 하는 경우, isDiscovering()을 사용하여 확인하면 됩니다.

각 앱의 manageMyConnectedSocket() 메서드는 데이터 전송을 위한 스레드를 시작하도록 설계되어 있습니다. 이 내용은 연결 관리 섹션에서 설명합니다.

BluetoothSocket 사용을 완료했으면 항상 close()를 호출합니다. 이렇게 하면 연결된 소켓이 즉시 닫히고 모든 관련된 내부 리소스가 해제됩니다.

연결 관리

여러 기기를 성공적으로 연결하고 나면 각 기기에 연결된 BluetoothSocket이 생깁니다. 이제 기기 간에 정보를 공유할 수 있으므로 본격적인 작업을 할 수 있습니다. BluetoothSocket을 사용하는 일반적인 데이터 전송 절차는 다음과 같습니다.

  1. getInputStream()getOutputStream()을 각각 사용하여 소켓을 통해 전송을 처리하는 InputStreamOutputStream을 가져옵니다.
  2. read(byte[])write(byte[])를 사용하여 스트림에 데이터를 읽고 씁니다.

물론 세부적인 구현을 고려해야 합니다. 특히, 스트림에서 읽고 쓰려면 전용 스레드를 사용해야 합니다. 이것이 중요한 이유는 read(byte[])write(byte[]) 메서드가 모두 차단 호출이기 때문입니다. read(byte[]) 메서드는 스트림에서 읽을 것이 생길 때까지 차단합니다. write(byte[]) 메서드는 보통 차단하지 않지만, 원격 기기가 read(byte[])를 신속하게 호출하지 않아 중간 버퍼가 가득 차면 흐름 제어를 위해 차단할 수 있습니다. 따라서 스레드의 기본 루프는 InputStream에서 읽기 전용으로 사용해야 합니다. 스레드의 개별 공용 메서드를 사용하여 OutputStream에 쓰기를 시작할 수 있습니다.

예시

연결된 두 기기 사이에서 블루투스를 통해 데이터를 전송하는 방법은 다음과 같습니다.

Kotlin

private const val TAG = "MY_APP_DEBUG_TAG"

// Defines several constants used when transmitting messages between the
// service and the UI.
const val MESSAGE_READ: Int = 0
const val MESSAGE_WRITE: Int = 1
const val MESSAGE_TOAST: Int = 2
// ... (Add other message types here as needed.)

class MyBluetoothService(
        // handler that gets info from Bluetooth service
        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 store for the stream

        override fun run() {
            var numBytes: Int // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                // Read from the InputStream.
                numBytes = try {
                    mmInStream.read(mmBuffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input stream was disconnected", e)
                    break
                }

                // Send the obtained bytes to the UI activity.
                val readMsg = handler.obtainMessage(
                        MESSAGE_READ, numBytes, -1,
                        mmBuffer)
                readMsg.sendToTarget()
            }
        }

        // Call this from the main activity to send data to the remote device.
        fun write(bytes: ByteArray) {
            try {
                mmOutStream.write(bytes)
            } catch (e: IOException) {
                Log.e(TAG, "Error occurred when sending data", e)

                // Send a failure message back to the activity.
                val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
                val bundle = Bundle().apply {
                    putString("toast", "Couldn't send data to the other device")
                }
                writeErrorMsg.data = bundle
                handler.sendMessage(writeErrorMsg)
                return
            }

            // Share the sent message with the UI activity.
            val writtenMsg = handler.obtainMessage(
                    MESSAGE_WRITE, -1, -1, mmBuffer)
            writtenMsg.sendToTarget()
        }

        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}

Java

public class MyBluetoothService {
    private static final String TAG = "MY_APP_DEBUG_TAG";
    private Handler handler; // handler that gets info from Bluetooth service

    // Defines several constants used when transmitting messages between the
    // service and the UI.
    private interface MessageConstants {
        public static final int MESSAGE_READ = 0;
        public static final int MESSAGE_WRITE = 1;
        public static final int MESSAGE_TOAST = 2;

        // ... (Add other message types here as needed.)
    }

    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private byte[] mmBuffer; // mmBuffer store for the stream

        public ConnectedThread(BluetoothSocket socket) {
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the input and output streams; using temp objects because
            // member streams are final.
            try {
                tmpIn = socket.getInputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating input stream", e);
            }
            try {
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when creating output stream", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
        }

        public void run() {
            mmBuffer = new byte[1024];
            int numBytes; // bytes returned from read()

            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                try {
                    // Read from the InputStream.
                    numBytes = mmInStream.read(mmBuffer);
                    // Send the obtained bytes to the UI activity.
                    Message readMsg = handler.obtainMessage(
                            MessageConstants.MESSAGE_READ, numBytes, -1,
                            mmBuffer);
                    readMsg.sendToTarget();
                } catch (IOException e) {
                    Log.d(TAG, "Input stream was disconnected", e);
                    break;
                }
            }
        }

        // Call this from the main activity to send data to the remote device.
        public void write(byte[] bytes) {
            try {
                mmOutStream.write(bytes);

                // Share the sent message with the UI activity.
                Message writtenMsg = handler.obtainMessage(
                        MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer);
                writtenMsg.sendToTarget();
            } catch (IOException e) {
                Log.e(TAG, "Error occurred when sending data", e);

                // Send a failure message back to the activity.
                Message writeErrorMsg =
                        handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
                Bundle bundle = new Bundle();
                bundle.putString("toast",
                        "Couldn't send data to the other device");
                writeErrorMsg.setData(bundle);
                handler.sendMessage(writeErrorMsg);
            }
        }

        // Call this method from the main activity to shut down the connection.
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Could not close the connect socket", e);
            }
        }
    }
}

생성자가 필요한 스트림을 얻고 나면 스레드는 InputStream을 통해 데이터가 들어오기를 기다립니다. read(byte[])가 스트림에서 데이터를 반환하면 해당 데이터는 부모 클래스의 Handler 멤버 핸들러를 사용하여 메인 Activity로 전송됩니다. 그러면 스레드가 InputStream에서 더 많은 바이트가 읽히도록 기다립니다.

나가는 데이터를 보내는 것은 메인 Activity에서 스레드의 write() 메서드를 호출하고 보낼 바이트를 전달하는 것만큼 간단합니다. 이 메서드는 write(byte[])를 호출하여 데이터를 원격 기기로 보냅니다. write(byte[])를 호출할 때 IOException이 발생할 경우, 스레드가 메인 Activity로 알림 메시지를 보내서 기기가 다른 (연결된) 기기로 바이트를 전송할 수 없다는 것을 사용자에게 설명합니다.

스레드의 cancel() 메서드가 언제든 연결을 종료할 수 있도록 BluetoothSocket을 닫습니다. 블루투스 연결 사용을 완료했을 때는 항상 이 메서드를 호출해야 합니다.

Bluetooth API 사용 데모는 블루투스 채팅 샘플 앱을 참조하세요.

기준 클래스와 인터페이스

모든 Bluetooth API는 android.bluetooth 패키지에서 구할 수 있습니다. 다음은 블루투스 연결을 설정하는 데 필요한 클래스 및 인터페이스에 대한 간략한 설명입니다.

BluetoothAdapter
로컬 블루투스 어댑터(블루투스 송수신 장치)를 나타냅니다. BluetoothAdapter는 블루투스 상호작용에 대한 진입점입니다. 이를 사용하여 다른 블루투스 기기를 검색하고 연결된(페어링된) 기기 목록을 쿼리하고 알려진 MAC 주소로 BluetoothDevice를 인스턴스화하고, 다른 기기로부터 통신을 수신 대기하는 BluetoothServerSocket을 만들 수 있습니다.
BluetoothDevice
원격 블루투스 기기를 나타냅니다. 이를 사용하여 BluetoothSocket을 통해 원격 기기와의 연결을 요청하거나 이름, 주소, 클래스 및 연결 상태와 같은 기기 정보를 쿼리합니다.
BluetoothSocket
블루투스 소켓(TCP Socket과 유사함)에 대한 인터페이스를 나타냅니다. 이는 InputStreamOutputStream을 통해 애플리케이션이 다른 블루투스 기기와 데이터를 교환할 수 있게 허용하는 연결 지점입니다.
BluetoothServerSocket
들어오는 요청을 수신 대기하는 열린 서버 소켓(TCPServerSocket과 유사함)을 나타냅니다. 두 대의 Android 기기를 연결하려면 한 기기가 이 클래스를 사용하여 서버 소켓을 열어야 합니다. 원격 블루투스 기기가 이 기기로 연결 요청을 보내면 해당 기기가 연결을 수락한 다음, 연결된 BluetoothSocket을 반환합니다.
BluetoothClass
블루투스 기기의 일반적인 특징 및 기능에 대해 설명합니다. 이는 기기 클래스와 서비스를 정의하는 읽기 전용 속성 집합입니다. 이 정보는 기기의 유형에 관한 유용한 힌트를 제공합니다. 다만 이 클래스의 특성만으로 기기가 지원하는 모든 블루투스 프로필과 서비스가 설명되는 것은 아닙니다.
BluetoothProfile
Bluetooth 프로필을 나타내는 인터페이스입니다. Bluetooth 프로필은 기기 간 블루투스 기반 통신에 대한 무선 인터페이스 사양입니다. 일례로 Hands-Free 프로필이 있습니다. 프로필에 대한 자세한 내용은 프로필 작업을 참조하세요.
BluetoothHeadset
블루투스 헤드셋을 휴대폰과 함께 사용할 수 있도록 지원합니다. 여기에는 Bluetooth Headset 프로필 및 Hands-Free(v1.5) 프로필이 포함됩니다.
BluetoothA2dp
A2DP(Advanced Audio Distribution Profile)을 사용하여 블루투스 연결을 통해 기기 간에 고품질 오디오를 스트리밍할 수 있는 방법을 정의합니다.
BluetoothHealth
블루투스 서비스를 제어하는 의료 기기 프로필 프록시를 나타냅니다.
BluetoothHealthCallback
BluetoothHealth 콜백을 구현하는 데 사용되는 추상 클래스입니다. 애플리케이션 등록 상태 및 블루투스 채널 상태의 변경에 대한 업데이트를 받으려면 이 클래스를 확장하고 콜백 메서드를 구현해야 합니다.
BluetoothHealthAppConfiguration
원격 블루투스 의료 기기와 통신하기 위해 타사 블루투스 의료 애플리케이션이 등록하는 애플리케이션 구성을 나타냅니다.
BluetoothProfile.ServiceListener
특정 프로필을 실행하는 내부 서비스와 연결하거나 연결을 끊을 때 BluetoothProfile IPC 클라이언트에 알리는 인터페이스입니다.