Android-Bluetooth

Bluetooth

블루투스 개요

  • 10m 정도의 가까운 거리에 있는 컴퓨터와 휴대폰, 가전제품등을 무선으로 연결하는 기술

블루투스 장점

  • 비교적 저렴한 저전력 근거리 무선 솔루션
  • 다른 무선 기술들에 비해 전자파 간섭 현상에 강함
  • 여러 대의 블루투스 디바이스들과 동시에 접속 가능
  • 무선랜과 달리 데이터와 음성 채널을 모두 가지고 있음
  • 크기가 작아 이동 통신용 단말기 등에 탑재가 용이

블루투스의 특징

  • 데이터 전송 속도 : 25Mbps(블루투스 4.0 버전)
  • 부품의 크기 : 1.2cm²의 크기
  • 저렴한 가격 : 5~10$
  • 작은 전력 소모 : 100mV 미만
  • 무선 주파수 : 2.4GHz 주파수 대역

블루투스 4.0 버전 부터 나누어진 기능

  • 클래식 블루투스 : 기존 블루투스 3.0 이하와 같은 기능 지원
  • 블루투스 하이 스피드 : 와이파이와 블루투스의 혼합칩을 사용, 와이파이의 속도로 수미터~수백미터의 원거리 통신이 가능
  • 블루투스 저전력 : 비교적 낮은 전력으로 블루투스 기능 지원

안드로이드 History

안드로이드 2.0

  • android.bluetooth 패키지 제공

    안드로이드 3.0

  • BluetoothProfile 인터페이스와 함께 BluetoothA2dp 클래스와 BluetoothHeadset 클래스 추가

    안드로이드 4.0

  • BluetoothHealth 클래스 추가

    안드로이드 4.3

  • 블루투스의 저전력 기능 지원을 위한 BluetoothGatt, BluetoothGattCallback, BluetoothGattCharacteristic, BluetoothGattService 클래스를 제공
  • 저전력 기능은 색다른 기능이 아닌 블루투스 검색시 비교적 저전력으로 수행할 수 있도록 만들어 준다.

블루투스 프로파일(Profile)

프로파일이란 블루투스 디바이스사이의 통신 규약(또는 프로토콜)을 말한다.
예를 들어 헤드셋이나 핸즈 프리 프로파일은 휴대전화에 연결된 헤드셋으로 통화할 수 있는 규약을 이야기함

중요 프로파일의 종류

  • A2DP(Advanced Audio Distribution Profile) : 스테레오 음질 수준의 오디오가 스트리밍될 수있는 방법을 설명합니다.
  • AVRCP(Audio/Video Remote Control Profile) : 텔레비전, 스테레오 오디오 장비 (Stereo Audio Equipment) 또는 다른 A/V 기기를 제어하기 위한 표준 인터페이스 (Standard Interface)를 제공하기 위해 설계되었습니다. 이 프로파일은 하나의 리모컨 (또는 다른 장치)로 사용자가 액세스할 수 있는 모든 A/V 기기를 제어 가능하게 합니다.
  • BPP (Basic Printing Profile) : 장치가 인쇄 작업에 따라 프린터에 텍스트, 이메일, V-카드, 이미지 또는 기타 정보를 보낼 수 있게 해줍니다
  • FTP (File Transfer Profile) : 서버 장치의 폴더 및 파일을 클라이언트 장치에 의해 탐색될 수 있는 방법을 정의합니다
  • GAP(Generic Access Profile) : 모든 블루투스 프로파일에 있어 기본적으로 적용되는 프로파일이다. GAP 프로파일은 블루투스 검색이나 디바이스간의 연결 처리를 정의하고 있다.
  • GATT(Generic Attribute Profile)블루투스 저전력 사용을 위한 프로파일이다. 저전력은 별도 속성 프로토콜(ATT)를 사용하기 때문에 경우에 따라 GATT/ATT라고 표현하기도 한다.
  • HFP(Hands-Free-Profile) : 휴대 전화의 음성 통신을 송수신 및 제어하는 프로파일이다.
  • HID(Human Interface Device Profile) : 키보드, 마우스, 멀티미디어, 조이스틱을 지원하는 프로파일이다.
  • HSP(Headset Profile) : 오디오를 지원하는 프로파일이다.
  • SPP(Serial Port Profile) : 시리얼 통신으로 데이터를 송수신하는 프로파일이다.
  • SDAP(Service Discovery Application Profile) : 블루투스 장치가 제공하는 서비스 정보를 파악할 때 사용하는 프로파일이다.
  • HDP(Health Device Profile) : 건강과 관련된 장치를 지원하는 프로파일이다.

위 프로파일 가운데 반드시 존재해야 하는 블루투스의 프로파일은 GAP와 SDAP이다.

GAP 프로파일

안드로이드를 포함하여 모든 블루투스 단말기는 GAP 프로파일을 지원하기 위해서 4가지 주요 작업들을 순차적으로 수행한다.

  1. 블루투스 디바이스의 활성화 작업
  2. 블루투스 디바이스를 검색하는 작업(옵션)
  3. 블루투스 디바이스들간에 연결하는 작업
  4. 블루투스 디바이스가 지원하는 서비스 정보의 전송과 수신 작업

안드로이드의 블루투스 권한

개발자가 안드로이드의 블루투스 기능을 사용하려면 AndroidManifest.xml파일에 다음과 같이 권한을 주어야 한다.

1
2
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  • BLUETOOTH 권한은 블루투스간의 연결 요구, 연결 승인, 데이터 전송등의 블루투스 통신을 하기 위해 필요한 권한이다.
  • BLUETOOTH_ADMIN 권한은 디바이스 검색을 시작하거나, 블루투스 설정 작업을 할 때 필요한 권한이다.

저전력 블루투스를 사용하려면 다음과 같은 사용 권한 역시 추가시켜 주어야 한다.

1
2
3
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

블루투스 활성화

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

1. BluetoothAdapter 가져오기

  • 모든 블루투스 액티비티를 위해 BluetoothAdapter가 필요.
  • BluetoothAdapter를 가져오려면 정적 getDefaultAdapter() 메서드를 호출.
  • getDefaultAdapter()가 null을 반환하는 경우 기기는 블루투스를 지원하지 않음.
1
2
3
4
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// Device does not support Bluetooth
}

BluetoothAdapter

  • 로컬 블루투스 어댑터(블루투스 송수신 장치)
  • BluetoothAdapter는 블루투스 상호작용에 대한 진입점
  • 이를 사용하여 다른 블루투스 기기를 검색하고 연결된(페어링된) 기기 목록을 쿼리할 수 있음
  • 또한 알려진 MAC 주소로 BluetoothDevice를 인스턴스화하고, 다른 기기로부터 통신을 수신 대기하는 BluetoothServerSocket을 만들 수 있음

2. 블루투스 활성화

  • isEnabled()를 호출하여 블루투스가 현재 활성화되었는지 확인.

bt enable request

  • 이 메서드가 false를 반환하는 경우 블루투스는 비활성화를 뜻함.
  • 블루투스 활성화를 요청하려면 ACTION_REQUEST_ENABLE 작업 인텐트를 사용하여 startActivityForResult()를 호출.
1
2
3
4
5
6
7
8
9
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

//or
if (!mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.enable(); //강제 활성화
}

블루투스 상태변화 브로드캐스트 수신

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

/* ... */

// Register for broadcasts on BluetoothAdapter state change
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
}

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

/* ... */

// Unregister broadcast listeners
unregisterReceiver(mReceiver);
}

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();

if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR); // EXTRA_STATE가 존재하지 않는다면 ERROR를 반환
switch (state) {
case BluetoothAdapter.STATE_OFF: //블루투스 비활성화
setButtonText("Bluetooth off");
break;
case BluetoothAdapter.STATE_TURNING_OFF: //비활성화 되어가고있음
setButtonText("Turning Bluetooth off...");
break;
case BluetoothAdapter.STATE_ON: //활성화
setButtonText("Bluetooth on");
break;
case BluetoothAdapter.STATE_TURNING_ON: //활성화 중
setButtonText("Turning Bluetooth on...");
break;
}
}
}
};

블루투스 검색과 페어링

  • BluetoothAdapter를 사용하면 기기 검색을 통해 또는 페어링된(연결된) 기기의 목록을 쿼리하여 원격 블루투스 기기를 찾을 수 있다.
  • 기기 검색은 블루투스 사용 기기에 대해 로컬 영역을 검색하고 각각에 대한 정보를 요청하는 검색 절차
  • 로컬 영역 내의 블루투스 기기는 검색 가능하도록 현재 활성화된 경우에만 검색 요청에 응답
  • 기기는 검색 가능한 경우 기기 이름, 클래스 및 고유 MAC 주소와 같은 정보를 공유

페어링

  • 원격 기기와 처음으로 연결되면 페어링 요청이 자동으로 사용자에게 제공
  • 기기가 페어링되면 해당 기기에 대한 기본 정보(예: 기기 이름, 클래스 및 MAC 주소)는 저장되고 Bluetooth API를 사용하여 읽을 수 있다.
  • 원격 기기에 대해 알려진 MAC 주소를 사용하면 (기기가 범위 내에 있다고 가정하여) 검색을 수행하지 않고 언제든지 연결을 시작할 수 있다.
  • 페어링은 두 기기가 서로의 존재를 알고 있고 인증에 사용할 수 있는 공유 링크 키를 가지고 있으며 서로 암호화된 연결을 설정할 수 있음을 의미
  • 연결은 기기가 현재 RFCOMM 채널을 공유하고 있고 데이터를 서로 전송할 수 있음을 의미

Android에서 페어링

  • 현재 Android Bluetooth API는 RFCOMM 연결을 설정할 수 있기 전에 기기를 페어링하도록 요청
  • Bluetooth API와 암호화된 연결을 시작하면 페어링이 자동으로 수행

페어링

페어링된 기기 쿼리

1
2
3
4
5
6
7
8
9
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); //페어링된 기기를 나타내는 BluetoothDevice 집합
// If there are paired devices
if (pairedDevices.size() > 0) {
// Loop through paired devices
for (BluetoothDevice device : pairedDevices) {
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "\n" + device.getAddress()); //ArrayAdapter를 사용하여 각 기기의 이름을 사용자에게 표시
}
}

페어링 요청

페어링 요청

1
2
3
4
5
6
7
8
9
10
11
12
public void pairDevice(BluetoothDevice device) {
String ACTION_PAIRING_REQUEST = "android.bluetooth.device.action.PAIRING_REQUEST";
Intent intent = new Intent(ACTION_PAIRING_REQUEST);

intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);

String EXTRA_PAIRING_VARIANT = "android.bluetooth.device.extra.PAIRING_VARIANT";
int PAIRING_VARIANT_PIN = 0; //핀코드를 0으로 함
intent.putExtra(EXTRA_PAIRING_VARIANT, PAIRING_VARIANT_PIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}

기기 검색

  • 기기 검색을 시작하려면 startDiscovery()를 호출
  • 이는 비동기 프로세스이며 해당 메서드는 검색이 성공적으로 시작했는지 여부를 나타내는 bool을 즉시 반환
  • 검색 프로세스는 일반적으로 12초 정도의 조회 스캔과, 블루투스 이름을 가져오는 검색된 각 기기의 페이지 스캔을 포함
1
2
3
4
5
6
7
// 현재 검색 진행중이면 검색을 취소시킴
if(mBluetoothAdapter.isDiscovering()){
mBluetoothAdapter.cancelDiscovery();
}

//BluetoothAdapter를 사용하여 검색 작업을 수행
mBluetoothAdapter.startDiscovery();

기기 검색에 따른 브로드캐스트

ACTION_DISCOVERY_STARTED & ACTION_DISCOVERY_FINISHED

  • ACTION_DISCOVERY_FINISHED : 장치 검색 시작
  • ACTION_DISCOVERY_FINISHED : 장치 검색 완료
1
2
3
4
5
6
7
8
9
10
11
12
13
BroadcastReceiver discoveryMonitor = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == BluetoothAdapter.ACTION_DISCOVERY_STARTED ) {
... // 장치 검색 시작
} else if (intent.getAction() == BluetoothAdapter.ACTION_DISCOVERY_FINISHED ) {
... // 장치 검색 완료
}
}
};

registerReceiver(discoveryMonitor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_STARTED));
registerReceiver(discoveryMonitor, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));

ACTION_FIOUND

  • 검색된 각 기기에 대한 정보를 수신
  • getName() : 블루투스 디바이스의 이름을 얻는다.
  • getAddress() : 블루투스 디바이스의 MAC 주소를 얻는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// When discovery finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice object from the Intent
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy

기기 검색기능 활성화

  • 로컬 기기를 다른 기기가 검색할 수 있게 하려면 ACTION_REQUEST_DISCOVERABLE 작업 인텐트를 사용하여 startActivityForResult(Intent, int)을 호출

검색 요청 허용 팝업

  • 기본적으로 기기가 120초 동안 검색 가능
  • EXTRA_DISCOVERABLE_DURATION 인텐트 엑스트라를 추가하여 다른 기간을 정의할 수 있다.
  • 앱이 설정할 수 있는 최대 기간은 3600초이며 값이 0인 경우 기기가 항상 검색 가능.
  • 0 미만 또는 3600 초과 값은 120초로 자동 설정
1
2
3
4
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); //300초 의미
startActivity(discoverableIntent);

블루투스 데이터 통신

  • 두 기기에서 애플리케이션 간 연결을 생성하려면 서버측 메커니즘과 클라이언트측 메커니즘을 모두 구현해야

  • 한 기기는 서버 소켓을 열고 다른 기기는 (서버 기기의 MAC 주소를 사용하여) 연결을 시작해야 하기 때문

  • 서버와 클라이언트는 각각 동일한 RFCOMM(프로토콜) 채널에 연결된 BluetoothSocket이 있는 경우 서로 연결된 것으로 간주

  • 서버는 들어오는 연결을 수락할 때 BluetoothSocket를 수신.

  • 클라이언트는 서버에 대한 RFCOMM 채널을 열 때 BluetoothSocket를 수신

블루투스 데이터 통신 서버

  • 두 기기를 연결하려면 한 기기는 열린 BluetoothServerSocket을 제공하여 서버로 작동해야
  • 서버 소켓의 목적은 들어오는 연결 요청을 수신 대기하고 수락 시 연결된 BluetoothSocket을 제공하는 것

BluetoothServerSocket 생성

  • listenUsingRfcommWithServiceRecord(String, UUID)를 호출하여 BluetoothServerSocket을 가져옴
  • 클라이언트는 이 기기와 연결을 시도할 때 연결할 서비스를 고유하게 식별하는 UUID를 제공
  • UUID는 클라이언트와 서버가 서로 일치해야함

UUID

  • TCP/UDP 프로토콜의 포트 번호처럼 블루투스가 제공하는 서비스를 식별하는 하나의 아이디로 사용
  • UUID 목록
  • 사용법은 BASE_UUID의 앞자리를 프로토콜 식별자에 맞게 바꾸어 준다.
    ex> 핸즈프리 0x111E 핸즈프리 프로파일(HFP)
    → 0000111E-0000-1000-8000-00805F9B34FB

연결 요청에 대한 응답

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

서버 소켓 close

  • 추가 연결을 수락하지 않으려면 close()를 호출
  • 그러면 서버 소켓과 모든 리소스가 해제되지만 accept()가 반환한 연결된 BluetoothSocket이 닫히지 않음
  • 대부분의 경우에 연결된 소켓을 수락한 직후에 BluetoothServerSocket에서 close()를 호출하는 것이 합당

연결을 수락하는 서버 구성 요소에 대한 간단한 스레드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException 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) {
break;
}
// If a connection was accepted
if (socket != null) {
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}

/** Will cancel the listening socket, and cause the thread to finish */
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}

클라이언트로 연결

  • 원격 기기(열린 서버 소켓을 제공하는 기기)와의 연결을 시작하려면 먼저 원격 기기를 나타내는 BluetoothDevice 객체를 가져와함
  • 그리고 나서 BluetoothDevice를 사용하여 BluetoothSocket을 가져오고 연결을 시작

BluetoothSocket 객체 얻기

  • BluetoothDevice를 통해 createRfcommSocketToServiceRecord(UUID)를 호출하여 BluetoothSocket을 가져온다.
  • connect()를 호출하여 연결을 시작
  • connect()를 호출할 때 기기 검색을 수행하지 않도록 해야 한다.

클라이언트로 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;

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

public void run() {
// Cancel discovery because it will slow down the connection
mBluetoothAdapter.cancelDiscovery();

try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.connect();
} catch (IOException connectException) {
// Unable to connect; close the socket and get out
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}

// Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
}

/** Will cancel an in-progress connection, and close the socket */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

블루투스 서비스 검색

안드로이드 4.0.3 버전 이상부터 UUID를 취득하기 위해 아래와 같은 함수들을 제공한다.

public boolean fetchUuidsWithSdp()

  • 위 메소드 호출시 안드로이드 프레임워크는 BluetoothDevice 객체가 가리키는 외부 블루투스의 SDP(Service Discovery Protocol) 내 데이터베이스로 저장하고 있는 서비스 이름과 UUID의 제공을 요청
  • 검색된 UUID는 ACTION_UUID라는 액션과 BluetoothDevice.EXTRA_UUID라는 엑스트라와 함께 브로드캐스트 인텐트를 통해 제공
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
    setSupportActionBar(myToolbar);

    btnScanDevice = (Button)findViewById(R.id.scandevice);
    out = (TextView) findViewById(R.id.out);

    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    filter.addAction(BluetoothDevice.ACTION_UUID);
    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
    registerReceiver(ActionFoundReceiver, filter);

    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if(bluetoothAdapter==null) {
    out.append("Bluetooth NOT supported. Aborting.\n");
    return;
    }

    out.append("Adapter: " + bluetoothAdapter);

    if (bluetoothAdapter.isEnabled()){
    if(bluetoothAdapter.isDiscovering()){
    out.append("블루투스는 현재 검색 진행중이다.");
    }else{
    out.append("블루투스는 이미 활성화되어 있습니다.");
    btnScanDevice.setEnabled(true);
    }
    }else{
    out.append("블루투스를 활성화시킵니다.!");
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

    btnScanDevice.setOnClickListener(ScanDevice);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    if (bluetoothAdapter != null && bluetoothAdapter.isDiscovering()) {
    bluetoothAdapter.cancelDiscovery();
    }
    unregisterReceiver(ActionFoundReceiver);
    }

    private final BroadcastReceiver ActionFoundReceiver = new BroadcastReceiver(){
    @Override
    public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (BluetoothDevice.ACTION_FOUND.equals(action)) {
    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    out.append("\n Device: " + device.getName() + ", " + device);
    btDeviceList.add(device);
    return;
    }

    if (BluetoothDevice.ACTION_UUID.equals(action)) {
    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    Parcelable[] uuidExtra = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
    for (int i=0; i<uuidExtra.length; i++) {
    out.append("\n Device: " + device.getName() + ", " + device + ", Service: " + uuidExtra[i].toString());
    }
    return;
    }

    if(BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
    out.append("\nDiscovery Started...");
    mProgressItem.setVisible(true);
    return;
    }

    if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
    out.append("\nDiscovery Finished");
    Iterator<BluetoothDevice> itr = btDeviceList.iterator();
    while (itr.hasNext()) {
    // Get Services for paired devices
    BluetoothDevice device = itr.next();
    out.append("\nGetting Services for " + device.getName() + ", " + device);
    if(!device.fetchUuidsWithSdp()) {
    out.append("\nSDP Failed for " + device.getName());
    }
    }

    out.append("\n페이링 디바이스로부터 UUID를 얻습니다.");
    Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
    for(BluetoothDevice device : pairedDevices) {
    out.append("\nGetting Services for " + device.getName() + ", " + device);
    if(!device.fetchUuidsWithSdp()) {
    out.append("\nSDP Failed for " + device.getName());
    }
    }
    mProgressItem.setVisible(false);
    }
    }
    };

public ParcelUuid[] getUUids()

  • getUUID() 메소드를 사용하여 다음과 같은 방법으로 블루투스 디바이스로부터 UUID를 얻을 수 있다.
    1
    2
    BluetoothDevice phoneDevice = bluetoothAdapter.getRemoteDevice(phoneAddress);
    ParcelUuid[] phoneUuids = phoneDevice.getUuids();

연결 관리

  • 두 대 이상의 기기를 성공적으로 연결한 경우 각 기기는 연결된 BluetoothSocket을 갖는다.
  • BluetoothSocket을 사용하여 임의의 데이터를 전송하는 일반적인 절차는 다음과 같다.
    • getInputStream() 및 getOutputStream()을 각각 사용하여 소켓을 통한 전송을 처리하는 InputStream 및 OutputStream을 가져온다.
    • read(byte[]) 및 write(byte[])을 사용하여 스트림에 데이터를 읽고 쓴다.
  • 모든 스트림 읽기 및 쓰기를 위한 전용 스레드를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;

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();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }

mmInStream = tmpIn;
mmOutStream = tmpOut;
}

public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()

// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}

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

/* Call this from the main activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

블루투스 프로필

  • Bluetooth 프로필은 기기 간 블루투스 기반 통신에 대한 무선 인터페이스 사양

    • 예로 Hands-Free 프로필이 있음.
    • 휴대폰이 무선 헤드셋에 연결하려면 두 기기가 Hands-Free 프로필을 지원해야 한다.
  • 인터페이스 BluetoothProfile을 구현하여 특정 Bluetooth 프로필을 지원하는 자신만의 클래스를 작성할 수 있다.

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

    • Android는 프로세스 간 통신(IPC)을 통해 블루투스 헤드셋 서비스를 제어하는 프록시인 BluetoothHeadset 클래스를 제공
    • Android는 IPC를 통해 블루투스 A2DP(Advanced Audio Distribution Profile) 서비스를 제어하는 프록시인 BluetoothA2dp 클래스를 제공(고품질 오디오가 기기 간 스트리밍)
    • 의료 기기 프로필
      • Bluetooth Health API는 BluetoothHealth, BluetoothHealthCallback 및 BluetoothHealthAppConfiguration 클래스를 포함
      • 이를 사용하면 블루투스를 사용하여 블루투스를 지원하는 의료 기기(예: 심박측정기, 혈압계, 체온계, 체중계)와 통신하는 애플리케이션을 만들 수 있음.
  • 기본적인 프로필 작업 단계

    1. 블루투스 설정에서 설명한 기본 어댑터를 가져온다.
    2. getProfileProxy()를 사용하여 프로필에 연결된 프로필 프록시 객체에 대한 연결을 설정.
    3. BluetoothProfile.ServiceListener를 설정.
    • 이 리스너는 서비스에 연결되었거나 연결이 끊어진 경우 BluetoothProfile IPC 클라이언트에 알린다.
    1. onServiceConnected()에서 프로필 프록시 객체에 대한 핸들을 가져온다.
    2. 프로필 프록시 객체가 있는 경우 해당 객체를 사용하여 연결 상태를 모니터링하고 해당 프로필과 관련된 다른 작업을 수행.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package com.android.btprofiles;

import android.app.Activity;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHealth;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class BluetoothProfiles extends Activity implements AdapterView.OnItemClickListener {
private final String TAG = getClass().getSimpleName();
private static final int REQUEST_ENABLE_BT = 1;
private ArrayAdapter<String> mConnectedDevices;
private ListView mList;
private BluetoothHeadset mBluetoothHeadset;
private BluetoothA2dp mBluetoothA2dp;
private BluetoothHealth mBluetoothHealth;
private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
private final List<BluetoothDevice> mVoicerecognizing = Collections.synchronizedList(new ArrayList<BluetoothDevice>());

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Bundle extras = intent.getExtras();
int state = extras.getInt(BluetoothProfile.EXTRA_STATE);
int prevState = extras.getInt(BluetoothProfile.EXTRA_PREVIOUS_STATE);
BluetoothDevice device = extras.getParcelable(BluetoothDevice.EXTRA_DEVICE);

if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
String stateStr = state == BluetoothProfile.STATE_CONNECTED ? "CONNECTED"
: state == BluetoothProfile.STATE_CONNECTING ? "CONNECTING"
: state == BluetoothProfile.STATE_DISCONNECTED ? "DISCONNECTED"
: state == BluetoothProfile.STATE_DISCONNECTING ? "DISCONNECTING"
: "Unknown";
String prevStateStr = prevState == BluetoothProfile.STATE_CONNECTED ? "CONNECTED"
: prevState == BluetoothProfile.STATE_CONNECTING ? "CONNECTING"
: prevState == BluetoothProfile.STATE_DISCONNECTED ? "DISCONNECTED"
: prevState == BluetoothProfile.STATE_DISCONNECTING ? "DISCONNECTING"
: "Unknown";
Log.d(TAG, "BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: EXTRA_DEVICE=" + device.getName() + " EXTRA_STATE=" + stateStr + " EXTRA_PREVIOUS_STATE=" + prevStateStr);
} else if (action.equals(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED)) {
String stateStr = state == BluetoothA2dp.STATE_NOT_PLAYING ? "NOT_PLAYING"
: state == BluetoothA2dp.STATE_PLAYING ? "PLAYING"
: "Unknown";
String prevStateStr = prevState == BluetoothA2dp.STATE_NOT_PLAYING ? "NOT_PLAYING"
: prevState == BluetoothA2dp.STATE_PLAYING ? "PLAYING"
: "Unknown";
Log.d(TAG, "BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED: EXTRA_DEVICE=" + device.getName() + " EXTRA_STATE=" + stateStr + " EXTRA_PREVIOUS_STATE=" + prevStateStr);
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
String stateStr = state == BluetoothHeadset.STATE_AUDIO_CONNECTED ? "AUDIO_CONNECTED"
: state == BluetoothHeadset.STATE_AUDIO_CONNECTING ? "AUDIO_CONNECTING"
: state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED ? "AUDIO_DISCONNECTED"
: "Unknown";
String prevStateStr = prevState == BluetoothHeadset.STATE_AUDIO_CONNECTED ? "AUDIO_CONNECTED"
: prevState == BluetoothHeadset.STATE_AUDIO_CONNECTING ? "AUDIO_CONNECTING"
: prevState == BluetoothHeadset.STATE_AUDIO_DISCONNECTED ? "AUDIO_DISCONNECTED"
: "Unknown";
Log.d(TAG, "BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED: EXTRA_DEVICE=" + device.getName() + " EXTRA_STATE=" + stateStr + " EXTRA_PREVIOUS_STATE=" + prevStateStr);
} else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
String stateStr = state == BluetoothHeadset.STATE_CONNECTED ? "CONNECTED"
: state == BluetoothHeadset.STATE_CONNECTING ? "CONNECTING"
: state == BluetoothHeadset.STATE_DISCONNECTED ? "DISCONNECTED"
: state == BluetoothHeadset.STATE_DISCONNECTING ? "DISCONNECTING"
: "Unknown";
String prevStateStr = prevState == BluetoothHeadset.STATE_CONNECTED ? "CONNECTED"
: prevState == BluetoothHeadset.STATE_CONNECTING ? "CONNECTING"
: prevState == BluetoothHeadset.STATE_DISCONNECTED ? "DISCONNECTED"
: prevState == BluetoothHeadset.STATE_DISCONNECTING ? "DISCONNECTING"
: "Unknown";
Log.d(TAG, "BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: EXTRA_DEVICE=" + device.getName() + " EXTRA_STATE=" + stateStr + " EXTRA_PREVIOUS_STATE=" + prevStateStr);
} else if (action.equals(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT)) {
String cmd = extras.getString(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
int type = extras.getInt(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE);
String[] args = extras.getStringArray(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
String typeStr = type == BluetoothHeadset.AT_CMD_TYPE_READ ? "AT_CMD_TYPE_READ"
: type == BluetoothHeadset.AT_CMD_TYPE_TEST ? "AT_CMD_TYPE_TEST"
: type == BluetoothHeadset.AT_CMD_TYPE_SET ? "AT_CMD_TYPE_SET"
: type == BluetoothHeadset.AT_CMD_TYPE_BASIC ? "AT_CMD_TYPE_BASIC"
: type == BluetoothHeadset.AT_CMD_TYPE_ACTION ? "AT_CMD_TYPE_ACTION"
: "Unknown";
String log = "BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT:";
log += " CMD=" + cmd;
log += " type=" + typeStr;
log += " args=";
for (int i = 0; i < args.length; i++) {
log += args[i];
if (i != args.length - 1) {
log += ",";
}
}
Log.d(TAG, log);
}
}
};

private final BluetoothProfile.ServiceListener mProfileListener = new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
String kind = null;
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = (BluetoothHeadset) proxy;
kind = "Headset";
} else if (profile == BluetoothProfile.A2DP) {
mBluetoothA2dp = (BluetoothA2dp) proxy;
kind = "A2DP";
} else if (profile == BluetoothProfile.HEALTH) {
mBluetoothHealth= (BluetoothHealth) proxy;
kind = "Health";
}

List<BluetoothDevice> devices = proxy.getConnectedDevices();
String[] names = new String[devices.size()];
for (int i = 0; i < names.length; i++) {
BluetoothDevice device = devices.get(i);
names[i] = kind + "\n" + device.getName() + "\n" + device.getAddress();
}
mConnectedDevices.addAll(names);
}

@Override
public void onServiceDisconnected(int profile) {
List<String> names = new LinkedList<String>();
for (int i = 0; i < mConnectedDevices.getCount(); i++) {
String name = mConnectedDevices.getItem(i);
if (profile == BluetoothProfile.HEADSET && name.startsWith("Headset")) {
names.add(name);
} else if (profile == BluetoothProfile.A2DP && name.startsWith("A2DP")) {
names.add(name);
} else if (profile == BluetoothProfile.HEALTH && name.startsWith("Health")) {
names.add(name);
}
}
for (String name : names) {
mConnectedDevices.remove(name);
}
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = null;
} else if (profile == BluetoothProfile.A2DP) {
mBluetoothA2dp = null;
} else if (profile == BluetoothProfile.HEALTH) {
mBluetoothHealth = null;
}
}
};

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

mConnectedDevices = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);

mList = (ListView) findViewById(R.id.connected);
mList.setAdapter(mConnectedDevices);
mList.setOnItemClickListener(this);
}

@Override
protected void onResume() {
super.onResume();
if (!mBluetoothAdapter.isEnabled()) {
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, REQUEST_ENABLE_BT);
}
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
registerReceiver(mReceiver, filter);
}

@Override
protected void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
closeProfiles();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}

@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.menu_get_profiles) {
getProfiles();
} else if (itemId == R.id.menu_close_profiles) {
closeProfiles();
}
return true;
}

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String text = ((TextView) view).getText().toString();
String kind= text.substring(0, text.indexOf("\n"));
String addr = text.substring(text.lastIndexOf("\n") + 1);
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(addr);
if (kind.equals("Headset")) {
boolean isAudioConnected = mBluetoothHeadset.isAudioConnected(device);
if (mVoicerecognizing.contains(device)) {
mVoicerecognizing.remove(device);
boolean result = mBluetoothHeadset.stopVoiceRecognition(device);
Toast.makeText(this, "isAudioConnected:" + isAudioConnected + " stopVoiceRecognition:" + result, Toast.LENGTH_LONG).show();
} else {
boolean result = mBluetoothHeadset.startVoiceRecognition(device);
if (result) {
mVoicerecognizing.add(device);
}
Toast.makeText(this, "isAudioConnected:" + isAudioConnected + " startVoiceRecognition:" + result, Toast.LENGTH_LONG).show();
}
} else if (kind.equals("A2DP")) {
Toast.makeText(this, "isA2dpPlaying:" + mBluetoothA2dp.isA2dpPlaying(device), Toast.LENGTH_LONG).show();
} else if (kind.equals("Health")) {
Toast.makeText(this, "Unsupported on this application", Toast.LENGTH_LONG).show();
}
}

private void getProfiles() {
mBluetoothAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET);
mBluetoothAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.A2DP);
mBluetoothAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.HEALTH);
}

private void closeProfiles() {
if (mBluetoothHeadset != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
mBluetoothHeadset = null;
}
if (mBluetoothA2dp != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mBluetoothA2dp);
mBluetoothA2dp = null;
}
if (mBluetoothHealth != null) {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEALTH, mBluetoothHealth);
mBluetoothHealth = null;
}
mConnectedDevices.clear();
}
}
공유하기