블루투스 개요

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

Android 애플리케이션은 블루투스 API를 사용하여 다음 작업을 할 수 있습니다.

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

이 페이지에서는 기본 블루투스를 중점적으로 설명합니다. 클래식 블루투스는 Android 기기 간 스트리밍 및 통신 등 배터리를 많이 사용하는 작업에 적합한 선택입니다. 전력 요구사항이 낮은 블루투스 기기를 위해 Android 4.3 (API 수준 18)에는 저전력 블루투스용 API 지원이 도입되었습니다. 자세한 내용은 저전력 블루투스를 참고하세요.

이 문서에서는 건강 기기 프로필을 비롯한 다양한 블루투스 프로필을 설명합니다. 그런 다음, Android Bluetooth API를 사용하여 블루투스를 사용하여 통신하는 데 필요한 네 가지 주요 작업, 즉 블루투스 설정, 로컬 영역에서 페어링되었거나 사용 가능한 기기 찾기, 기기 연결, 기기 간 데이터 전송 작업을 수행하는 방법을 설명합니다.

기본사항

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

블루투스 권한

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

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

Android 10 이상에서 실행되는 서비스는 ACCESS_BACKGROUND_LOCATION 권한이 없으면 블루투스 기기를 검색할 수 없습니다. 이 요구사항에 관한 자세한 내용은 백그라운드에서 위치 정보 액세스를 참고하세요.

다음 코드 스니펫은 권한을 확인하는 방법을 보여줍니다.

Kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    if (ContextCompat.checkSelfPermission(baseContext,
        Manifest.permission.ACCESS_BACKGROUND_LOCATION)
        != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
                    PERMISSION_CODE)
        }
}

Java

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  if (ContextCompat.checkSelfPermission(baseContext,
      Manifest.permission.ACCESS_BACKGROUND_LOCATION)
      != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(
              MyActivity.this,
              new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION},
                  PERMISSION_CODE)
      }
}

단, 앱이 Android 11 이상을 실행하는 기기에 설치되어 있고 호환 기기 페어링을 사용하여 기기를 연결한 경우는 이 권한 요구사항의 예외입니다. 이 경우 기기가 연결되면 앱이 위치 정보 액세스 권한을 요청하지 않고 연결된 블루투스 기기를 검색할 수 있습니다.

Android 8.0 (API 수준 26) 이상을 실행하는 기기에서는 CompanionDeviceManager를 사용하여 위치 정보 액세스 권한 없이 앱을 대신하여 근처 호환 기기를 스캔할 수 있습니다. 이 옵션에 관한 자세한 내용은 호환 기기 페어링을 참고하세요.

참고: 앱이 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는 블루투스 프로필 작업을 지원합니다. 블루투스 프로필은 기기 간 블루투스 기반 통신을 위한 무선 인터페이스 사양입니다. Hands-Free 프로필을 예로 들 수 있습니다. 휴대전화를 무선 헤드셋에 연결하려면 두 기기 모두 Hands-Free 프로필을 지원해야 합니다.

Android Bluetooth API는 다음 블루투스 프로필의 구현을 제공합니다.

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

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

  1. 블루투스 설정에 설명된 대로 기본 어댑터를 가져옵니다.
  2. BluetoothProfile.ServiceListener를 설정합니다. 이 리스너는 서비스에 연결되었거나 연결 해제되었을 때 BluetoothProfile 클라이언트에 알립니다.
  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 인텐트의 broadcast receiver를 만들어 헤드셋에 관한 공급업체별 AT 명령어를 처리합니다.

의료 기기 프로필

Android 4.0 (API 수준 14)은 블루투스 의료 기기 프로필 (HDP)을 지원합니다. 이를 통해 블루투스를 사용하여 블루투스를 지원하는 의료 기기(예: 심박수 모니터, 혈압계, 체온계, 체중계)와 통신하는 애플리케이션을 만들 수 있습니다. Bluetooth Health API에는 주요 클래스 및 인터페이스에 설명된 BluetoothHealth, BluetoothHealthCallback, BluetoothHealthAppConfiguration 클래스가 포함되어 있습니다.

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를 사용하여 두 단계로 진행됩니다.

  1. BluetoothAdapter를 가져옵니다.

    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 매개변수로 다시 전달합니다.

    블루투스 사용 설정에 성공하면 활동이 onActivityResult() 콜백에서 RESULT_OK 결과 코드를 수신합니다. 오류로 인해 블루투스가 사용 설정되지 않은 경우 (또는 사용자가 '아니요'를 응답한 경우) 결과 코드는 RESULT_CANCELED입니다.

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

팁: 검색 기능을 사용 설정하면 블루투스가 자동으로 사용 설정됩니다. 블루투스 활동을 실행하기 전에 기기 검색 기능을 일관되게 사용 설정하려면 위의 2단계를 건너뛰어도 됩니다. 자세한 내용은 이 페이지의 검색 기능 사용 설정 섹션을 참조하세요.

기기 찾기

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

기기 검색은 로컬 영역에서 블루투스 지원 기기가 있는지 검색하고 각 기기에 관한 일부 정보를 요청하는 검색 절차입니다. 이 프로세스는 검색, 문의 또는 스캔이라고도 합니다. 하지만 근처 블루투스 기기는 검색 가능 상태를 통해 정보 요청을 수락하는 경우에만 검색 요청에 응답합니다. 기기를 검색할 수 있으면 기기 이름, 클래스, 고유 MAC 주소와 같은 정보를 공유하여 검색 요청에 응답합니다. 그러면 이 정보를 사용하여 검색 프로세스를 실행하는 기기가 검색된 기기와의 연결을 시작할 수 있습니다.

검색 가능한 기기는 사용자 위치 정보를 공개할 수 있으므로 기기 검색 프로세스에 위치 액세스 권한이 필요합니다. 앱이 Android 8.0 (API 수준 26) 이상을 실행하는 기기에서 사용 중인 경우 호환 Device Manager API를 사용하세요. 이 API는 앱을 대신하여 기기 검색을 실행하므로 앱에서 위치 정보 액세스 권한을 요청할 필요가 없습니다.

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

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

  • 페어링이란 두 기기가 서로의 존재를 알고 있고, 인증에 사용할 수 있는 공유 링크 키를 보유하고 있으며, 서로 암호화된 연결을 설정할 수 있다는 의미입니다.
  • 연결된다는 것은 기기가 현재 RFCOMM 채널을 공유하고 서로 데이터를 전송할 수 있음을 의미합니다. 현재 Android Bluetooth API는 RFCOMM 연결을 설정하기 전에 기기를 페어링하도록 요구합니다. 블루투스 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 객체에서 getAddress()를 호출하여 검색할 MAC 주소만 있으면 됩니다. 연결 만들기에 관한 자세한 내용은 기기 연결 섹션을 참고하세요.

주의: 기기 검색을 실행하면 블루투스 어댑터의 리소스가 많이 소모됩니다. 연결할 기기를 찾은 후에는 연결을 시도하기 전에 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 객체에서 getAddress()를 호출하여 검색하는 MAC 주소만 있으면 됩니다. 연결 만들기에 관한 자세한 내용은 기기 연결 섹션을 참고하세요.

주의: 기기 검색을 실행하면 블루투스 어댑터의 리소스가 많이 소모됩니다. 연결할 기기를 찾은 후에는 연결을 시도하기 전에 cancelDiscovery()를 사용하여 검색을 중지해야 합니다. 또한 기기에 연결된 상태에서는 검색을 실행하면 안 됩니다. 검색 프로세스로 인해 기존 연결에 사용할 수 있는 대역폭이 크게 줄어들기 때문입니다.

검색 기능 활성화

로컬 기기를 다른 기기에서 검색할 수 있게 하려면 ACTION_REQUEST_DISCOVERABLE 인텐트로 startActivityForResult(Intent, int)를 호출합니다. 그러면 자체 앱이 중지되는 설정 앱으로 이동할 필요 없이 시스템의 검색 가능 모드를 사용 설정하도록 요청이 전송됩니다. 기본적으로 기기는 120초 또는 2분 동안 검색 가능합니다. EXTRA_DISCOVERABLE_DURATION를 더 추가하여 다른 기간을 최대 3,600초 (1시간)까지 정의할 수 있습니다.

주의: 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'라고 응답하면 기기는 지정된 시간 동안 검색 가능한 상태가 됩니다. 그러면 활동은 onActivityResult() 콜백 호출을 수신하고, 결과 코드는 기기가 검색 가능한 기간과 동일합니다. 사용자가 '아니요'라고 응답하거나 오류가 발생한 경우 결과 코드는 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: 블루투스 페어링 대화상자

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

서버로 연결

두 기기를 연결하려면 열린 BluetoothServerSocket를 보유하여 한 기기가 서버 역할을 해야 합니다. 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 요청이 수락된 후 연결된 BluetoothSocket를 제공하는 것입니다. BluetoothServerSocket에서 BluetoothSocket를 가져오는 경우 기기가 추가 연결을 수락하도록 하지 않는 한 BluetoothServerSocket는 삭제될 수 있으며 삭제되어야 합니다.

서버 소켓을 설정하고 연결을 수락하려면 다음 단계를 순서대로 완료하세요.

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

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

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

  2. accept()를 호출하여 연결 요청 리슨을 시작합니다.

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

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

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

accept() 호출은 차단 호출이므로 애플리케이션이 다른 사용자 상호작용에 계속 응답할 수 있도록 기본 활동 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를 가져옵니다.

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

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

    클라이언트가 이 메서드를 호출하면 시스템은 SDP 조회를 실행하여 일치하는 UUID가 있는 원격 기기를 찾습니다. 조회에 성공하고 원격 기기가 연결을 수락하면 연결 중에 사용할 RFCOMM 채널을 공유하고 connect() 메서드가 반환됩니다. 연결이 실패하거나 connect() 메서드의 시간이 약 12초 후에 타임아웃되면 메서드에서 IOException이 발생합니다.

    connect()는 차단 호출이므로 항상 기본 활동 (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, bytes)
            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, bytes);
                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 멤버를 사용하여 기본 활동으로 전송됩니다. 그런 다음 스레드는 InputStream에서 더 많은 바이트를 읽을 때까지 기다립니다.

발신 데이터를 보내는 것은 기본 활동에서 스레드의 write() 메서드를 호출하고 전송할 바이트를 전달하는 것만큼 간단합니다. 이 메서드는 write(byte[])를 호출하여 데이터를 원격 기기로 전송합니다. write(byte[])를 호출할 때 IOException이 발생하면 스레드는 기본 활동에 토스트 메시지를 보내 기기가 지정된 바이트를 다른 연결된 기기로 보낼 수 없다고 사용자에게 설명합니다.

스레드의 cancel() 메서드를 사용하면 BluetoothSocket를 닫아 언제든지 연결을 종료할 수 있습니다. 블루투스 연결 사용을 마쳤으면 항상 이 메서드를 호출해야 합니다.

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

기준 클래스와 인터페이스

모든 블루투스 API는 android.bluetooth 패키지에서 사용할 수 있습니다. 다음은 블루투스 연결을 설정하는 데 필요한 클래스 및 인터페이스를 요약한 것입니다.

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