검색한 기기에 연결하기 위해서는 ConnectThread와 ConnectedThread에 대해서 알고 있어야 한다.
BluetoothService.java 파일에 ConnectThread와 ConnectedThread 클래스를 내부 클래스로 삽입해준다.
1. ConnectThread
BluetoothService.java
public class BluetoothService {
... // 이전 부분 생략
// RFCOMM Protocol
private static final UUID MY_UUID =
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothAdapter btAdapter;
private ConnectThread mConnectThread;
private ConnectedThread mConnectedThread;
... // 이전 부분 생략
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
// 디바이스 정보를 얻어서 BluetoothSocket 생성
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "create() failed", e);
}
mmSocket = tmp;
}
public void run() {
Log.i(TAG, "BEGIN mConnectThread");
setName("ConnectThread");
// 연결을 시도하기 전에는 항상 기기 검색을 중지한다.
// 기기 검색이 계속되면 연결속도가 느려지기 때문이다.
btAdapter.cancelDiscovery();
// BluetoothSocket 연결 시도
try {
// BluetoothSocket 연결 시도에 대한 return 값은 succes 또는 exception이다.
mmSocket.connect();
Log.d(TAG, "Connect Success");
} catch (IOException e) {
connectionFailed(); // 연결 실패시 불러오는 메소드
Log.d(TAG, "Connect Fail");
// socket을 닫는다.
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG,
"unable to close() socket during connection failure",
e2);
}
// 연결중? 혹은 연결 대기상태인 메소드를 호출한다.
BluetoothService.this.start();
return;
}
// ConnectThread 클래스를 reset한다.
synchronized (BluetoothService.this) {
mConnectThread = null;
}
// ConnectThread를 시작한다.
connected(mmSocket, mmDevice);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
}
여기서 UUID라는 변수가 있는데 이것은 Bluetooth 통신 프로토콜이다. 블루투스 통신을 할 때 그에 맞는 프로토콜로 통신을 해 주어야 한다. 간단한 예로 블루투스 기능이 지원되는 이어폰과의 통신을 원한다면 그에 맞는 프로토콜을 사용하여야 하고, 스마트폰 끼리의 블루투스 통신은 또 그에 맞는 프로토콜을 사용해야 한다.
지금 위에서 사용한 프로토콜은 아마 스마트폰끼리의 블루투스 통신을 하는데 사용하는 프로토콜일 것이다.
위에서(ConnectThread) 다음단계가 ConnectedThread이다. 위 단계에서는 연결을 하는 중간단계 까지라고 생각하고, 이번 단계는 연결을 완료하는 단계라고 생각하면 될 것 같다. 위와 동일하게 BluetoothService 클래스에 내부 클래스로 정의한다.
BluetoothService.java
public class BluetoothService {
... // 이 전 부분 생략
private BluetoothAdapter btAdapter;
private Activity mActivity;
private Handler mHandler;
private ConnectThread mConnectThread; // 변수명 다시
private ConnectedThread mConnectedThread; // 변수명 다시
... // 이 전 부분 생략
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
Log.d(TAG, "create ConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// BluetoothSocket의 inputstream 과 outputstream을 얻는다.
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (true) {
try {
// InputStream으로부터 값을 받는 읽는 부분(값을 받는다)
bytes = mmInStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* Write to the connected OutStream.
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
// 값을 쓰는 부분(값을 보낸다)
mmOutStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
}
3. 마무리 작업
지금까지 잘 따라했다면 몇 가지 빨간줄이 있을 것이다. 이 빨간줄을 지워보자
기본 베이스는 처음에 말했듯이 안드로이드 기본 예제인 BluetoothChat이다.
BluetoothChat에서 다음과 같은 메소드를 모두 복사해서 BluetoothService.java에 붙여넣어 빨간줄을 없애보자
BluetoothService.java
public class BluetoothService {
... // 이 전과 동일
private int mState;
// 상태를 나타내는 상태 변수
private static final int STATE_NONE = 0; // we're doing nothing
private static final int STATE_LISTEN = 1; // now listening for incoming connections
private static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
private static final int STATE_CONNECTED = 3; // now connected to a remote device
... // 이 전과 동일
// Bluetooth 상태 set
private synchronized void setState(int state) {
Log.d(TAG, "setState() " + mState + " -> " + state);
mState = state;
}
// Bluetooth 상태 get
public synchronized int getState() {
return mState;
}
public synchronized void start() {
Log.d(TAG, "start");
// Cancel any thread attempting to make a connection
if (mConnectThread == null) {
} else {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread == null) {
} else {
mConnectedThread.cancel();
mConnectedThread = null;
}
}
// ConnectThread 초기화 device의 모든 연결 제거
public synchronized void connect(BluetoothDevice device) {
Log.d(TAG, "connect to: " + device);
// Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread == null) {
} else {
mConnectThread.cancel();
mConnectThread = null;
}
}
// Cancel any thread currently running a connection
if (mConnectedThread == null) {
} else {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to connect with the given device
mConnectThread = new ConnectThread(device);
mConnectThread.start();
setState(STATE_CONNECTING);
}
// ConnectedThread 초기화
public synchronized void connected(BluetoothSocket socket,
BluetoothDevice device) {
Log.d(TAG, "connected");
// Cancel the thread that completed the connection
if (mConnectThread == null) {
} else {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread == null) {
} else {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
setState(STATE_CONNECTED);
}
// 모든 thread stop
public synchronized void stop() {
Log.d(TAG, "stop");
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
setState(STATE_NONE);
}
// 값을 쓰는 부분(보내는 부분)
public void write(byte[] out) { // Create temporary object
ConnectedThread r; // Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED)
return;
r = mConnectedThread;
} // Perform the write unsynchronized r.write(out); }
}
// 연결 실패했을때
private void connectionFailed() {
setState(STATE_LISTEN);
}
// 연결을 잃었을 때
private void connectionLost() {
setState(STATE_LISTEN);
}
private class ConnectThread extends Thread {
...
}
private class ConnectedThread extends Thread {
...
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult " + resultCode);
switch (requestCode) {
/** 추가된 부분 시작 **/
case REQUEST_CONNECT_DEVICE:
// When DeviceListActivity returns with a device to connect
if (resultCode == Activity.RESULT_OK) {
btService.getDeviceInfo(data);
}
break;
/** 추가된 부분 끝 **/
case REQUEST_ENABLE_BT:
// When the request to enable Bluetooth returns
if (resultCode == Activity.RESULT_OK) {
// Next Step
btService.scanDevice();
} else {
Log.d(TAG, "Bluetooth is not enabled");
}
break;
}
}
requestCode가 REQUEST_CONNECT_DEVICE인 경우 getDeviceInfo()메소드를 호출하도록 하였다.
getDeviceInfo()메소드는 아직 구현하지 않았다.
이 메소드는 기기선택 액티비티에서 선택한 기기의 정보를 받아 getDeviceInfo()라는 메소드로 전달을 해주고 getDeviceInfo()메소드는 그 정보를 이용하여 블루투스연결을 시도할 예정이다.
3. 검색된 기기에 접속하기 - 1
검색된 기기에 접속하기 위해서 getDeviceInfo()메소드를 작성해보자.
BluetoothService.java
public void getDeviceInfo(Intent data) {
// Get the device MAC address
String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
// Get the BluetoothDevice object
//BluetoothDevice device = btAdapter.getRemoteDevice(address);
BluetoothDevice device = btAdapter.getRemoteDevice(address);
Log.d(TAG, "Get Device Info \n" + "address : " + address);
connect(device);
}
address라는 String에는 선택한 기기의 주소가 담겨져 있고, 그 주소를 BluetoothDevice로 전달하여 connect()메소드로 전달하면 접속을 할 수 있다. connect()메소드는 아직 구현을 하지 않았다. 이제 connect()메소드만 구현하면 기기의 블루투스 연결은 가능할 것 이다.
4. 검색된 기기에 접속하기 - 2
이제부터는 많은 양의 복사 붙여넣기를 시도할 예정이다.
안드로이드 블루투스 채팅 예제의 BluetoothChatService의 connect()메소드를 붙여넣고, 이와 관련된 모든 소스를 붙여넣는다.
MainActivity에는 Button과 TextView만 배치해놓고 BluetoothService라는 Class를 만들도록 한다.
BluetoothService라는 Class에서 블루투스와 관련된 모든 작업을 처리할 예정이다.
작업이 처리되는 과정은 핸들러를 통해서 MainActivity에 전달되어 상태값을 전달받아 TextView에 보여주거나 버튼의 상태를 변경해 주도록 할 것이다.
BluetoothService 생성자는 메인이 되는 MainActivity로 부터 Activity와 Handler 값을 받는다.
BluetoothService.java
public class BluetoothService {
// Debugging
private static final String TAG = "BluetoothService";
private BluetoothAdapter btAdapter;
private Activity mActivity;
private Handler mHandler;
// Constructors
public BluetoothService(Activity ac, Handler h) {
mActivity = ac;
mHandler = h;
// BluetoothAdapter 얻기
btAdapter = BluetoothAdapter.getDefaultAdapter();
}
MainActivity.java
private BluetoothService btService = null;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// BluetoothService 클래스 생성
if(btService == null) {
btService = new BluetoothService(this, mHandler);
}
}
5. 블루투스 활성화
블루투스를 활성화 하기 위해 BluetoothAdapter클래스를 사용해서 다음 두 단계의 작업을 순서대로 진행한다.
① 블루투스 지원 확인
② 블루투스 활성화
① 블루투스 지원 확인
블루투스 통신을 사용하기 전에 디바이스가 블루투스를 지원하는지 확인할 필요가 있다. 디바이스가 블루투스를 지원하는지 확인해보도록 하자
BluetoothService.java
public boolean getDeviceState() {
Log.d(TAG, "Check the Bluetooth support");
if(btAdapter == null) {
Log.d(TAG, "Bluetooth is not available");
return false;
} else {
Log.d(TAG, "Bluetooth is available");
return true;
}
}
getDeviceState() 라는 메소드를 만들어서 기기의 블루투스 지원여부를 확인 한다.
BluetoothAdapter가 null일 경우 블루투스 통신을 지원하지 않는 기기이다. (하지만 이러한 기기는 요즘 없을듯..?)
② 블루투스 활성화
getDeviceState()가 true를 반환할 경우 블루투스 활성화를 요청하도록 해보자.
블루투스 활성화 요청을 위해서 enableBluetooth()라는 메소드를 만들었다.
BluetoothService.java
public void enableBluetooth() {
Log.i(TAG, "Check the enabled Bluetooth");
if(btAdapter.isEnabled()) {
// 기기의 블루투스 상태가 On인 경우
Log.d(TAG, "Bluetooth Enable Now");
// Next Step
} else {
// 기기의 블루투스 상태가 Off인 경우
Log.d(TAG, "Bluetooth Enable Request");
Intent i = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
mActivity.startActivityForResult(i, REQUEST_ENABLE_BT);
}
}
기기의 블루투스 상태가 On일 경우 다음 단계를 실행하면 되고,
기기의 블루투스 상태가 Off일 경우 블루투스 활성화를 요청하는 알림창을 띄운다.
알림창에서 확인/취소를 선택할 경우 결과는 MainActivity에 onActivityResult()메소드로 들어온다.
그 전에 startActivityForResult의 개념에 대해서 알고 있어야 이해하기 편한데, 이 것은 구글링을 통해서 알아보는것을 추천한다.
알림창의 확인/취소 결과는 MainActivity에 onActivityResult()메소드로 전달되는데 소스를 보면 다음과 같다.
MainActivity.java
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_BT:
// When the request to enable Bluetooth returns
if (resultCode == Activity.RESULT_OK) {
// 확인 눌렀을 때
//Next Step
} else {
// 취소 눌렀을 때
Log.d(TAG, "Bluetooth is not enabled");
}
break;
}
}
이제 MainActivity에서 버튼을 클릭했을 때 블루투스 활성화를 위한 이벤트 처리만 해주면 된다.
MainActivity.java
@Override
public void onClick(View v) {
if(btService.getDeviceState()) {
// 블루투스가 지원 가능한 기기일 때
btService.enableBluetooth();
} else {
finish();
}
}
여기까지 블루투스를 활성화 하는 방법에 대해서 알아보았다.
블루투스가 활성화 되었을 때(resultCode가 Activity.RESULT_OK일때) 기기 검색을 통해 페어링을 시켜주면 되는데, 이 부분은 다음장에 포스팅 하도록 하겠다.
앱의 기능은 구글링을 통해서 어느정도 구현을 할 수가 있는데, 똑같은 기능을 갖고 있는 앱 중에서 UI가 이쁜 앱과 UI가 평범한 앱중에는 당연히 UI가 이쁜 앱을 쓰지 않을까??
독특한 특색을 갖는 UI를 꾸미기 위해서 이번에는 액션바를 활용해 보겠다. (커스텀하게 액션바를 만드는 내용은 다음에 시간날때 올리도록 하겠다..)
글쓴이의 경우 앱에 액션바를 사용하였는데 액션바에 배경이미지를 넣고 사용하기 위해 커스텀하게 구현을 하였다. 버튼역시 테마에 맞게 이미지를 넣어 구현을 하였다.
이때 문제가 발생하였는데 이미지를 넣은 버튼을 배치했을 때 디바이스에 따라 해상도가 모두 다르기 때문에 버튼의 크기가 제 각각 이었다.
액션바의 크기는 모든 디바이스에서 비율이 똑같을 것이라고 생각하고 크기를 절대값으로 조절하였더니 문제가 생긴것이다. 해상도에 따라서 액션바의 크기도 제각각이었다. 그래서 액션바의 크기를 구해서 버튼크기를 조절하려고 했다. 그러나 액션바의 크기가 0으로 나와서 크기를 알 수 가 없었다.
그렇다.. 액션바의 크기는 0이었던 것이다..
하지만 액션바의 크기를 구할 수 있는 방법이 있다!!
액티비티가 실행될 때 onCreate 안에서 액션바의 크기를 구하면 0이 나오지만, 스레드를 이용하면 크기를 구할수 있다.
일단 Action Bar 와 Status Bar 크기를 구하는 코드부터 살펴보자
private void getStatusBarSize() {
Rect rectgle = new Rect();
Window window = getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectgle);
int StatusBarHeight = rectgle.top;
int contentViewTop = window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
int TitleBarHeight = contentViewTop - StatusBarHeight;
Log.i("getHeight", "StatusBar Height= " + StatusBarHeight + " TitleBar Height = " + TitleBarHeight);
}
위의 메소드를 onCreate 안에서 호출하지말고 스레드를 통해서 호출한다.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LinearLayout layout = new LinearLayout(this);
layout.post(new Runnable() {
@Override
public void run() {
getStatusBarSize();
}
});
}
스레드를 통해서 getStatusBarSize를 호출하면 크기를 구할 수 있다.
여기서 또 한가지 팁은 액션바 사이즈를 구해서 액션바 사이즈를 이용해서 액션바에 들어가는 버튼이나 이미지 크기를 조절하려면 onCreate안에서 계산하면 안된다. 스레드를 통해서 크기를 구하기 때문에 onCreate가 끝나고 크기가 구해질 수 가 있기 때문에 액션바에 들어가는 버튼의 크기를 조절하고자 한다면 스레드 안에서 getStatusBarSize()를 호출하고 난 뒤에 크기를 조절해 주도록 하자!