2012. 10. 1. 18:47

[출처 :  정말 죄송합니다 복사하고 어딘지 놓쳤습니다. 알려주시면 감사하겠습니다. ]


블루투스 권한 설정

 

앱에서 블루투스 기능을 사용하기 위해서는 먼저 적어도 BLUETOOTH, BLUETOOTH_ADMIN 중 하나는 선언해야 한다.

연결, 연결 수락, 데이터 교환 등의 블루투스 통신을 진행하고 싶으면 BLUETOOTH 권한을 요청해야 하며 블루투스 설정을 조작하거나 초기 장비를 검색하기 위해서는 BLUETOOTH_ADMIN 권한을 요청해야 한다.

 

대부분의 앱은 로컬 블루투스 장비들을 검색하기 위해 전적으로 이 권한을 필요하다. 만약 사용자가 블루투스 설정을 변경하는 파워 관리자일 필요가 없다면 이 권한을 요청할 필요는 없다. 만약 BLUETOOTH_ADMIN 권한을 가지면 BLUETOOTH 권한에서 허가된 기능도 수행할 수 있다.

 

앱의 매니페스트에 블루투스 권한을 선언은 다음과 같이 <uses-permission> 엘리먼트를 사용하면 된다.

 

<manifest ...]] >

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

...

</manifest>

 

블루투스 설정

 

앱이 블루투스 상에서 통신을 하기 이전에 먼저 블루투스가 장비에서 지원하는지와 활성화되어 있는지를 검증해야 한다.

 

만약 블루투스가 지원되지 않는다면 앱의 어떠한 블루투스 기능도 자연스럽게 비활성화 시켜야 하며 만약 블루투스가 지원되지만 비활성화되어져 있다면 앱을 떠나지 않고 블루투스를 활성화 시키도록 요청할 수 있다.

 

이러한 작업은 BluetoothAdapter 를 사용하여 처리한다.

 

1. BluetoothAdapter 획득

 

BluetoothAdapter는 어떠한 블루투스 액티비티에서 요청할 수 있다. BluetoothAdpater를 얻기 위해서는 static getDefaultAdapter() 메서드를 호출한다. 이 메서드는 장비가 소유하고 있는 블루투스 어댑터에 대한 BluetoothAdapter 객체를 반환하며 이 어댑터는 전체 시스템에서 하나만 존재한다. 그리고 앱은 이 객체를 사용하여 어댑터와 연동할 수 있다. 만약 getDefaultAdapter()가 null을 반환하면 장비는 블루투스를 지원하지 않는 것이며 블루투스에 관한 어떠한 작업도 할 수 없다.

 

BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}

 

2. 블루투스 활성화

 

다음으로 블루투스를 활성화 시켜야 하는데 먼저 isEnabled() 메서드를 통해 현재 활성화 상태인지를 확인할 수 있다. 만약 false이면 블루투스가 비활성화 되어져 있는 것으로 활성화 상태로 변경하려면 ACTION_REQUEST_ENABLE 액션 인텐트와 함께 startActivityForResult()를 호출해야 한다. 이것은 앱의 중지 없이 시스템의 설정을 통해 블루투스를 활성화 시키는데 목적이 있는 요청이다.

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

 

위의 코드가 실행되면 블루투스를 활성화 시키기 위한 사용자 권한을 요청하는 다이얼로그 장이 표시되며 사용자가 "Yes"를 선택하면 시스템은 블루투스를 활성화 시키는 작업을 수행하며 활성화가 완료되면 앱으로 다시 복귀된다.

 

만약 블루투스 활성화가 성공하면 복귀된 액티비티는 OnActivityResult() 메서드에서 result code에 RESULT_OK를 받을 것이고 만약 "No"를 선택하여 블루투스를 활성화 시키지 않았다면 result code는 RESULT_CANCELED 이다.

 

옵션으로 앱은 시스템에서 블루투스 상태가 변경되는 사항을 알아내기 위해 ACTION_STATE_CHANGED 브로드캐스트 인텐트를 받을 수 있도록 설정할 수 있다. 이 브로드캐스트는 EXTRA_STATE와 EXTRA_PREVIOUS_STATE 에 대한 정보가 포함되어져 있으며 이는 각각 이전과 새롭게 설정된 상태를 나타낸다. 이 extra 데이터에 허용되는 값은 STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, STATE_OFF 가 있으며 이 데이터를 앱이 실행되고 있는 동안 블루투스 상태가 변화되는 것을 대처할 수 있다.

 

검색 활성화를 동작 시키면 자동으로 블루투스가 활성화될 것이다. 만약 이것을 블루투스 액티비티가 진행되기 전에 설정하면 2. 블루투스 활성화는 처리하지 않아도 된다.

 

창비 찾기

 

BluetoothAdapter를 사용하면 장비 탐색 혹은 페어링된 장비리스트를 질의하는 방법으로 원격의 블루투스 장비들을 찾을 수 있다.

 

장비 탐색은 로컬 영역에서 활성화된 블루투스 장비를 찾는 스캐닝 작업을 수행하고 각각에 대해 몇가지 정보를 요청한다. (때때로 이것은 발견, 탐구 혹은 스캔닝 이라고 한다.) 그러나 이것도 로컬 영역 내의 블루투스 장비가 탐색시 발견될 수 있도록 활성화 되어져 있을 때만 가능하다.

 

만약 장비가 탐색되면 장비 이름, 클래스 그리고 유일한 MAC 주소와 같은 공유 정보를 가지고 탐색 요청에 대해 응답할 것이다. 이 정보는 탐색 작업을 수행중인 장비는 탐색된 장비와의 연결을 초기화하는데 사용할 수 있다.

 

처음으로 원격 장비와 연결자를 만들 때 사용자에게 자동으로 페어링 요청이 보여진다. 장비가 페어링될 때 장비의 기본 정보(장비 이름, 클래스 그리고 MAC 주소)는 저장되고 그후 블루투스 API들을 통해 읽을 수 있다.

 

원격 장비가 잘 알려진 MAC 주소를 사용할 경우 같은 범위안에 있다면 연결자를 탐색 작업 없이 어떤때든지 초기화할 수 있다.

 

페어링됨과 연결됨의 차잇점을 알아야 한다. 페어링된다는 것은 2개의 장비가 인증에서 사용할 수 있는 link-key 를 공유하고 각각 상대방과 암호화된 연결자를 수립할 수 있는 조건을 갖추는 등 서로의 존재를 아는 것을 말한다.

 

연결된다는 것은 장비가 현재 RFCOMM 채널을 공유하고 각각의 장비 간에 데이터를 송수신할 수 있는 것을 말한다.

현재 안드로이드 블루투스 API들은 장비들이 RFCOMM 연결이 되기 이전에 페어링되기를 요청하고 있다. (페어링은 당신이 블루투스 API들을 이용해 암호화된 연결자를 초기화할 때 자동으로 진행된다.)

 

다음 섹션은 페어링된 장비들을 찾는 방법과 장비 탐색을 통해 새로운 장비를 탐색하는 것을 설명하겠다.

 

참고

안드로이드 장비는 기본적으로 탐색되지 않게 되어져 있다. 사용자는 환경설정을 통해 제한된 시간동안 장비가 탐색될 수 있도록 하거나 앱이 앱의 동작을 멈추지 않고 사용자가 탐색 기능을 활성할 수 있도록 요청할 수 있다.

 

페어링된 장비들을 질의하기

 

장비 탐색을 진행하기에 앞서 만약 연결하고자 하는 장비가 이미 페어링된 것인지를 페어링된 장비들의 집합을 질의할 필요가 있다. 이 작업은 getBondedDevices() 메서드를 호출하는 것으로 페어링된 장비들을 나타내는 BluetoothDevice들의 집합을 반환한다. 예를 들어 다음은 모든 페어링된 장비들을 질의하고 사용자에게 ArrayAdapter를 이용하여 장비의 이름을 보여주는 코드이다.

 

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// 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());
    }
}

 

BluetoothDevice 객체로 부터 연결을 위해 필요한 것은 MAC 주소이다. 여기서는 단순히 사용자에게 장비의 MAC 주소를 표시하지만 뒤에 연결을 초기화하는 데 사용하되는 것을 자세히 살펴보도록 하겠다.

 

장비들 탐색

 

장비 탐색을 시작하기 위해서는 단순히 startDiscovery() 를 호출하면 된다. 이 메서드의 진행은 비동기적이고 탐색이 성공적으로 시작되었는지 아닌지에 대한 boolean 형식의 결과값을 반환한다. 탐색 작업은 보통 12초 정도이며 페이지에는 스캔해서 찾은 장비들의 블루투스 이름을 표시한다.

 

앱은 각 장비 탐색에 대한 정보를 받으려면 ACTION_FOUND 인텐트에 대한 브로드캐스트 리시버를 등록해야 한다. 장비를 찾을 때마다 시스템은 ACTION_FOUND 인텐트를 브로드캐스트 한다.이 인텐트는 BluetoothDevice 객체가 있는 EXTRA_DEVICE와 BluetoothClass 객체가 있는 EXTRA_CLASS 를 포함하고 있다.

 

브로드캐스트 리시버에 대한 예제는 다음과 같다.

// 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

 

연결을 초기화하는데 필요한 정보인 MAC 주소는 BluetoothDevice 객체에 있다.

 

주의

장비 탐색 진행은 블루투스 어댑터에게 무겁고 많은 자원을 소비하는 작업이다. 당신이 연결하기 위한 장비를 찾았다면 항상 연결을 시도하기 전에 cancelDiscovery() 호출을 통해 탐색 작업을 중지시켜야 한다. 또한 만약 장비와 연결시 장비 탐색을 중지시키지 않는다면 연결에 대한 대역폭 활용을 감소시키므로 반드시 연결되어있는 동안은 탐색 작업이 되지 않도록 해야 한다.

 

탐색 활성화

 

만약 장비를 다른 장비들에 탐색될 수 있도록 만들고 싶다면 ACTION_REQUEST_DISCOVERABLE 액션 인텐트와 함께 startActivityForResult(Intent, int)를 호출한다. 이것은 앱이 중지되지 않고 환경설정의 탐색 모드를 활성화 시킨다.

 

기본적으로 장비는 120초 동안 탐색될 수 있는 상태가 되지만 이를 변경하고 싶다면 EXTRA_DISCOVERABLE_DURATION의 extra 값을 인텐트에 추가하여 호출하면 된다. 앱은 최고 3600 초을 설정할 수 있다. 그리고 0값이면 항상 탐색 모드를 유지하겠다는 의미이다. 만약 0미만이거나 3600 보다 크면 자동으로 120초로 설정된다.

 

다음은 300초로 설정하는 예제이다.

 

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

 

위의 코드가 실행되면 다음과 같이 장비가 탐색될 수 있도록 하는 사용자 권한을 요청하는 다이얼로그가 보여지며 "Yes"를 선택하면 특정한 시간(여기서는 300초)동안 탐색 가능 상태가 된다.

 

사용자의 선택 결과 값은 액티비티의 onActivityResult()를 통해 전달 받으며 사용자가 "No"를 선택했거나 에러가 발생했으면 Activity.RESULT_CANCELED 가 결과 코드로 전달될 것이다.

 

참고

만약 블루투스가 장비에 활성화되어 있지 않다면 장비 탐색 가능 모드 작업은 자동으로 활성화 시킬 것이다.

 

장비는 자동으로 일정 시간동안 탐색 가능 모드를 유지할 것이고 만약 탐색 가능 모드에 대한 설정이 변하는 것을 알아내고 싶다면 ACTION_SCAN_MODE_CHANGED 인텐트에 대한 브로드캐스트 리시버를 등록하면 된다.

 

전달되는 인텐트안에는 현재의 스캔 모드인 EXTRA_SAN_MODE 와 이전 스캔 모드인 EXTRA_PREVIOUS_SCAN_MODE가 있다. 여기에 가능한 값은 SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE 또는 SCAN_MODE_NONE 이다. 이것은 각각 탐색 가능 모드, 연결을 받아들일 수 있는 탐색 불가능 모드, 연결을 받아들일 수 없는 탐색 불가능 모드를 나타낸다.

 

만약 원격 장비와 연결을 초기화 할 것이면 장비 탐색을 활성화할 필요는 없다. 앱은 원격 장비를 탐색하여 연결을 초기화할 수 있기 때문에 장비 탐색 활성화는 오직 앱에서 서버 소켓을 통해 연결을 수락할 때에만 필요하다.

 

장비와의 연결

 

2개의 장비에서 실행되고 있는 앱 사이 연결자를 생성하기 위해서는 서버 사이드와 클라이언트 사이드를 구현해야 한다.

 

하나의 장비는 서버 소켓을 열고 다른 한 장비는 서버 장비의 MAC 주소를 사용하여 연결자를 초기화해야 한다. 서버와 클라이언트는 같은 RFCOMM 채널에 대한 연결된 BluetoothSocket을 가질 때 연결되었다고 할 수 있으며 이것을 통해 각 장비는 input과 output 스트림들을 얻어 데이터 전송을 시작할 수 있다. 이것에 대한 것은 연결자 관리에서 자세히 설명하고 여기서는 2개의 장비간에 연결자를 초기화하는 법을 살펴보겠다.

 

서버 장비와 클라이언트 장비는 각각 다른 방법으로 BluetoothSocket을 얻는다. 서버는 연결을 받아들일 때 얻고 클라이언트는 서버에 대한 RFCOMM 채널을 오픈할 때 얻는다.

 

구현 기술 중 하나는 각각 장비가 서버 소켓을 오픈하고 연결 요청을 수락하는 서버와 같이 준비시키고 그 다음 장비들은 상대방 장비와 클라이언트로서 연결을 시작하는 것이다. 아니면 다른 구현 방법은 명시적으로 "host" 로서 수용하는 서버 소켓을 오픈하고 다른 장비는 단순히 연결을 시작하는 것이다.

 

참고

만약 2개의 장비가 이전에 페어링되지 않았다면 안드로이드 프레임워크는 자동으로 페어링 요청 노티피케이션을 보여주거나 연결을 수행하는 동안 사용자에게 다이얼로그를 보여준다. 그래서 장비들에 연결하기 위해 수락될 때 앱은 장비들이 페어링되었는지 아닌지를 신경쓸 필요가 없다. RFCOMM 연결은 사용자가 성공적으로 페어링될 때 까지 블럭킹되거나 사용자의 페어링 취소 혹은 타임 아웃으로 실패될 것이다.

 

서버로 연결

 

2개의 장비를 연결하려면 하나는 서버로서 BluetoothServerSocket을 오픈하여야 한다.

서버 소켓은 연결 요청을 듣기 위함이고 하나가 연결을 될 때 연결된 BluetoothSocket을 제공받는다.

BluetoothSocket이 BluetoothServerSocket으로 부터 획득되면 BluetoothServerSocket은 더이상 연결을 받지 않아도 된다면 폐기시켜도 괜찮다.

 

서버 소켓을 설정하고 연결을 수락하는 방법은 다음과 같다.

 

1. listenUsingRfcommWithServiceRecord(String, UUID)메서드를 호출하여 BluetoothServerSoket을 얻는다.

listenUsingRfcommWithServiceRecord(String, UUID)메서드의 String 형식의 파라메터는 앱에서 제공하는 서비스를 구분시킬 수 있는 문자열로서 시스템은 자동으로 장비에 Service Discovery Protocol(SDP) 데이터베이스 엔트리를 저장한다. (이름은 문자를 사용하면 되고 단순히 앱의 이름을 사용해도 된다.) UUID는 또한 SDP 엔트리 안에 포함되어 저장되고 클라이언트 장비에 대한 연결 동의에 대해 기본 정보가 된다. 클라이언트는 연결을 원하는 서비스을 유일하게 구분하는 UUID 정보를 함께 보내 이 장비에 연결을 시도할 것이다. UUID가 매칭이 될 때 연결이 수락된다.

UUID에 대해서

Universally Unique Identifier(UUID)는 정보가 유일하게 구분되게 하는데 사용되는 문자열로 된 ID로 128-bit 형식을 가져야 한다.UUID의 포인트는 랜덤으로 선택해도 충돌하지 않을 정도로 충분히 크다는 것이다. 이 경우에는 앱의 블루투스 서비스를 구분하는데 사용하는 것이다. 당신은 UUID 클래스의 fromString(String)을 사용하여 생성할 수 있다.

 

2. accept() 메서드를 호출하여 연결 요청을 듣기 시작한다.

 

이것은 블럭킹 호출이며 연결이 수락되거나 예외사항이 발생할 때 반환한다.

연결은 원격 장비가 서버 소켓을 생성할 때 등록한 UUID와 매칭되는 UUID를 가지고 연결 요청을 보낼 때만 연결을 수락한다. accept()는 연결을 수락하면 원격 장비와 연결된 BluetoothSocket 객체를 반환한다.

 

3. 만약 추가적인 연결을 수락하지 않으려면 close() 메서드를 호출한다.

 

이 메서드는 서버 소켓을 릴리즈 하고 모든 자원을 시스템을 반환한다. 그러나 accept()에 의해 반환된 연결된 BluetoothSocket은 종료되지 않는다.

이러한 모든 BluetoothServerSocket 혹은 BluetoothSocket에 관한 작업은 앱에서 별도의 쓰레드를 사용하여 처리한다. BluetoothServerSocket과 BluetoothSocket의 모든 메서드들은 thread-safe 하다.

다음은 쓰레드로 연결을 수락하는 서버 컴포넌트를 구현한 예제이다.

 

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) { }
    }
}

 

위의 예제는 오직 하나의 연결만 허용하는데 연결이 수락되자 마자 BluetoothSocket을 획득하고 획득한 BluetoothSocket을 별도의 쓰레드로 보내고 클라이언트 코드에서 하는 것과 같은 connect() 메서드를 호출하지 않는다.

 

manageConnectedSocket() 은 앱에서 데이터를 송수신하기 위한 쓰레드를 초기화하는 메서드이다. 이 함수는 뒤의 연결 관리에서 설명하도록 하겠다.

일반적으로 예제에서와 같이 연결을 받자 마자 BluetoothServerSocket을 종료한다. 이것은 close() 메서드를 호출하면 되고 또한 별도의 서버 소켓을 종료할 수 있는 별도의 public 메서드를 정의하여 다른 쓰레드에서 호출하여 종료시킬 수도 있다.

 

클라이언트로 연결

 

원격 장비에 연결하기 위해 먼저 원격 장비에 대한 BluetoothDevice 객체를 얻어야 한다. (BluetoothDevice 객체를 찾는 방법은 위의 장비 찾기 섹션을 참조하기 바란다.)

BluetoothDevice를 통해 BluetoothSocket을 얻고 연결을 시도한다.

 

다음은 이에 대한 기본 흐름이다.

 

1. BluetoothDevice의 createRfcommSocketToServiceRecord(UUID) 메서드를 통해 BluetoothSocket을 얻는다.

 

이 메서드는 BluetoothDevice에 연결할 수 있도록 BluetoothSocket을 초기화하며 여기에 파라메터로 보내는 UUID는 서버에서 BluetoothServerSocket을 오픈할 때 사용한 UUID와 일치해야 한다 (listenUsingRfcommWithServiceRecord(String, UUID)). 같은 UUID 문자열을을 하드코딩하고 서버 코드와 클라이언트 코드에서 공유하여 사용하는 것이 좋다.

 

2. connect() 메서드를 호출하여 연결을 시도한다.

 

이 메서드 호출은 시스템이 UUID를 매치시키기 위하여 원격 장비의 SDP를 조회한다. 만약 성공적으로 조회되면 원격 장비는 연결을 수락하고 연결되어 있는 동안 사용할 수 있는 RFCOMM 채널을 공유하고 메서드를 마친다. 만약 연결 실패 혹은 타임아웃(대략 12초)이 되면 예외사항이 발생한다.

connect() 메서드는 블럭킹 호출이므로 항상 메인 액티비티 쓰레드와 별도의 쓰레드로 동작해야 한다.

 

다음은 블루투스 연결을 시도하는 쓰레드의 기본 코드이다.

 

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) { }
    }
}

 

연결을 시도하기에 앞서 먼저 cancelDiscovery()를 호출한 것을 주의깊게 살펴보기 바란다. 당신은 장비 검색이 진행이 되고 있던 아니던지 안전하게 처리하려면 항상 이 메서드를 호출해야 한다. (만약 체크를 먼저 하고 싶으면 isCovering() 메서드를 호출)

 

manageConnectedSocket()은 데이터를 송수신하는 쓰레드를 초기화하는 앱 안에서의 메서드이다. 이것은 다음 섹션에서 설명하도록 하겠다.

BluetoothSocket을 다 사용하면 항상 close() 메서드를 호출하여 자원을 시스템에 되돌려 줘야 한다.

 

연결 관리

 

성공적으로 2개의 장비가 연결되면 양쪽 다 BluetoothSocket을 갖게 된다. 이것을 통해 장비 간에 데이터를 공유하면서 재미있는 기능을 만들어 나갈 수 있다.

데이터를 주고 받는 것은 의외로 간단하다.

 

1. 소켓을 통해 송수신을 핸들링 할 수 있는 InputStream과 OutputStream을 getInputStream()과 getOutputStream()을 통해 얻는다.

 

2. 이 스트림들의 read(byte[])와 write(byte[]) 메서드를 통해 데이터를 읽고 쓴다.

 

이것이 데이터 송수신의 모든 것이다. 물론 구현은 좀더 상세하게 고려해야 할 것이다.

 

먼저 모든 스트림을 읽고 쓰는 작업은 별도의 쓰레드를 선언하여 구현한다. 왜냐 하면 read(byte[])와 write(byte[]) 메서드 둘다 블럭킹 호출이기 때문이다.

 

read(byte[]) 메서드는 스트림으로 부터 읽기를 마칠 때까지 블럭킹된다. write(byte[]) 메서드는 보통 블럭킹되지 않지만 만약 원격 장비에서 충분히 빠르게 read(byte[]) 메서드를 호출하지 않고 버퍼가 꽉 찰 경우 흐름에 따라 블러킹될 수 있다. 그래서 쓰레드의 메인 루프에서는 InputStream에서 데이터를 읽는 작업을 하고 쓰레드의 별도 public 메서드를 선언하여 OutputStream에 데이터를 쓰는 작업을 처리하도록 구현한다.

 

다음은 위에서 설명한 프로세스에 대한 간단한 예시이다.

 

/p>

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) { }
    }
}

 

생성자에서 한번만 필요한 스트림을 얻고 쓰레드 동작(run())에서 InputStream을 통해 데이터가 도착할 때까지 대기한다.

 

스트림으로 부터 byte[] 데이터가 도착하면 read(byte[]) 메서드는 즉시 반환하고 부모 클래스의 멤버 변수로 있는 Handler를 통해 메인 액티비티에 데이터를 전달한다.

그 다음 다시 스트림으로 부터 다른 byte[] 데이터가 도착할 때 대기한다.

 

데이터를 보내는 것은 메인 액티비티에서 보낼 byte[] 데이터를 파라메터로 하여 쓰레드의 write() 메서드를 호출하도록 구현되어져 있다. 이 메서드는 원격 장비에 데이터를 전송하기 위해 write(byte[]) 메서드를 호출한다.

 

쓰레드의 cancel() 메서드는 언제든지 BluetoothSocket을 종료해야 하므로 아주 중요한 메서드이다. 이 메서드는 모든 블루투스 연결을 통해 진행되는 작업이 마쳐지면 호출되어져야 한다.

 

Profile를 사용하여 작업하기

 

안드로이드 3.0을 시작으로 블루투스 API는 블루투스 Profile을 사용하여 작업할 수 있는 지원사항이 포함되어졌다.

 

블루투스 Profile은 장비간 블루투스 기반 통신에 대해 무선 인터페이스 규약(표준)서이다. 예를 들어 모바일 폰과 무선 헤드셋을 연결하는 Hands-Free profile 이 있다. 양쪽의 장비가 Hands-Free profile을 지원한다면 손쉽게 연결될 것이다.

 

당신은 특정한 블루투스 profile을 지원하기 위해 BluetoothProfile 인터페이스에 대해 클래스들을 구현할 수 있다.

안드로이드 블루투스 API는 다음과 같은 블루투스 profile들에 대해 구현물을 제공하고 있다.

 

  • Headset - Headset profile은 블루투스 헤드셋을 모바일 폰에서 사용할 수 있도록 지원한다. 안드로이드는 BluetoothHeadset 클래스를 제공하며 IPC(Interprocess communication)을 통해 블루투스 Headset 서비스를 제어하는 프록시 클래스 이다. 이것은 블루투스 Headset과 Hands-Free (v1.5) profile을 포함하고 있다. BluetoothHeadset 클래스는 AT 명령어들에 대한 지원 사항이 포함되어져 있다. 좀더 자세한 사항은 Vendor-specific AT commands을 보기 바란다.
  • A2DP (Advanced Audio Distribution Profile) - A2DP는 고품질 오디오가 블루투스 연결을 통해 장비간 전달될 수 있는 방법을 정의하고 있다. 안드로이드는 IPC를 통해 블루투스 A2DP 서비스를 제어할 수 있는 BluetoothA2dp 프록시 클래스를 제공하고 있다.

 

Profile을 사용하여 작업하는 기본 프로세스는 다음과 같다.

 

  1. 블루투스 설정 섹션에서 설명한 방법으로 기본 어댑터를 얻는다.
  2. getProfileProxy() 메서드를 사용하여 profile과 연관된 프록시 객체와 연결하도록 시도한다. 아래 예제의 프록시 객체는 BluetoothHeadset 인스턴스이다.
  3. BluetoothProfile.ServiceListener를 설정한다. 이 리스너는 서버스로 부터 연결되거나 연결 해제될 때 BluetoothProfile IPC 클라이언트들에게 알려주는 역할을 수행한다.
  4. onServiceConnected() 메서드에 profile 프록시 객체를 얻는다.
  5. 일단 profile 프록시 객체를 가지면 연결 상태를 모니터할 수 있고 profile과 연관된 다른 작업을 수행할 수 있다.

 

다음은 headset profile을 제어할 수 있는 BluetoothHeadset 프록시 객체를 얻는 방법에 대한 예시이다.

 

BluetoothHeadset mBluetoothHeadset;

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

// Establish connection to the proxy.
mBluetoothAdapter.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET);

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

// ... call functions on mBluetoothHeadset

// Close proxy connection after use.
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);

 

Vecdor-specific AT commands

 

안드로이드 3.0 부터 headset들에 의해 전송되는 미리 정의된 vendor-specific AT 명령어들(Plantronics +XEVENT 명령어 등)의 시스템 브로드캐스트를 받을 수 있도록 등록할 수 있다.

 

예를 들어 앱은 연결된 장비의 배터리 량을 가리키는 브로드캐스트들을 수신받을 수 있고 사용자에 대해 전달받을 수 있으며 필요한 다른 액션을 획득할 수 있다.

 

headset에 대한 vendor-specific AT 명령어들을 핸들링하기 위해서는 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT 인텐트에 대한 브로드캐스트 리시버를 생성하여 등록하면 된다.


Posted by k1rha