공부/안드로이드(Android)

블루투스에 대해서 - 채팅 프로젝트

도도-도윤 2017. 11. 24. 18:51

블루투스에 대해서 - 채팅 프로젝트


블루투스 채팅에 대해서 소개합니다.

C++, C언어, C# 등의 언어에서 네트워크 프로그래밍을 다뤄보면, 소켓을 여는 원리랑 같습니다.

다만 그것보다는 복잡할 것 같습니다.



1. 디자인 패턴(Singleton)

먼저 소스코드를 소개하기에 앞서, 객체지향 프로그래밍의 패턴을 소개합니다.


 public class Animal{

          private static final Animal instance;


          private Animal(){
          }


          public Animal getInstance(){

                if ( instance == null )

                      return new Animal();

                return instance;

          }


 }

 싱글톤 패턴(Singleton)

가볍게 디자인패턴을 다뤄봤습니다. 조금 더 복잡할지도 모르지만......



2. 블루투스

먼저 블루투스(Bluetooth)는 근거리 무선 통신 기술을 이야기 합니다.

거리는 15m 내외~100m 이내입니다.  IEEE 802.15.1에 정의되어 있는 스팩입니다.

블루투스는 1대1 무선 통신을 위해 설계된 프로토콜입니다.


 "위키피디아에 있는 블루투스 설명" 요약

 블루투스는, 통신에 이용되는 전파의 강도를 클래스라는 개념으로 규정하고 있다.
 블루투스 지원 장비는, 어떤 장비든 한 클래스로 분류된다. 쌍방이 같은 클래스일 필요는 없다.


* 블루투스 클래스

클래스

출력

도달 거리

Class 1

100mW

100m

Class 2

2.5mW

10m

Class 3

1mW

1m

Class 4

0.5mW

~0.5m


IEEE에서는 규격명 IEEE 802.15.1으로 등재되어 있으나, 현재 블루투스는 Bluetooth Special Interest Group (SIG)을 통해 관리되고 있다. 이 그룹에는 전기통신, 컴퓨터, 네트워크, 가전 등의 분야의 30,000사 이상의 기업들이 멤버에 가입되어 있다.[3] 블루투스 SIG는 규격의 개발을 감시, 규격의 인증 프로그램의 관리 및 트레이드마크의 보호를 관장하고 있다. [4] 장비 제조사가 블루투스 장비로 인증을 받기 위해서는, SIG에서 제정한 표준 규격을 만족해야 한다.

~~~


 [연혁]

 1994년에 에릭슨의 사내 프로젝트로 개발 시작하였다.~

[프로파일]

블루투스는, 여러 종류의 장비로의 통신에 사용되는 규격인 이유로, 장비의 종류에 따라 규정되는 각각의 별도의 프로토콜이 존재한다. 이들 프로토콜의 사용법을 프로파일이라는 용어로 표준화하고 있다. 통신하고자 하는 장비와 장비 간에, 동일한 프로파일을 가지고 있는 경우에만, 그 프로파일을 이용한 통신이 가능하다.

대표적인 프로파일은 다음과 같은 것들이 있다.

GAP (Generic Access Profile)
블루투스 장비의 접속/인증/암호화를 규정하는 프로토콜.
SDAP (Service Discovery Application Profile)
다른 블루투스 장비가 제공하는 기능(프로파일)을 참조하는 용도의 프로토콜.
DUN (Dial-up Networking Profile)
휴대전화 등을 통해 인터넷에 다이얼 업 접속을 할때 사용되는 프로파일
FTP (File Transfer Profile)
컴퓨터 사이의 데이터 통신을 위한 프로파일. 참고로 파일전용용 프로토콜 FTP와는 관계 없음.
HID (Human Interface Device Profile)
컴퓨터 마우스 또는 키보드 등의 입력장비와 무선으로 연결하기 위한 프로파일
HSP (Headset Profile)
블루투스를 내장한 헤드셋과의 통신을 위한 프로파일. 모노 음성 수신 뿐만 아니라, 마이크로의 쌍방 통신도 규정.
HFP (Hands-Free Profile)
차내 또는 헤드셋을 통한 핸즈프리 통화를 위한 프로파일. HSP 기능에 추가로 통신의 발신/착신 기능을 규정.
A2DP (Advanced Audio Distribution Profile)
음성을 리시버가 달린 헤드폰(또는 이어폰)으로 전송하는 프로파일. HSP/HFP가 모노 음성인 것에 반해, 스테레오 고음질 음성을 지원.
AVRCP (Audio/Video Remote Control Profile)
AV기기의 리모콘 기능을 구현하는 프로파일


 [블루투스 사양]


 블루투스 4.0 + LE

 이 주제의 자세한 내용은 Bluetooth Low Energy 문서를 참고하십시오.

Bluetooth SIG는 블루투스 사양서 버전 4.0(Bluetooth Smart)을 2010년 6월 30일에 채택하였다. 이 사양에는 클래식 블루투스(Classic Bluetooth), Bluetooth high speed 와 Bluetooth low energy 프로토콜이 포함되었다. Bluetooth high speed 는 Wi-Fi 를 바탕으로, 클래식 블루투스는 기존의 레거시 블루투스 프로토콜을 바탕으로 한다.

한편, 종래의 버전과 비교해 대폭적으로 소비전력을 낮춘 Bluetooth Low Energy 는, Bluetooth SIG 공개자료에 의하면, 버튼형 전지 1개만으로도 수년간 구동 가능하도록 되어 있다. 전송 속도는 1Mbps로, 데이터 패킷 사이즈가 8 - 27옥테드로 매우 작아졌다. 가전제품 등에 탑재된 센서와의 데이터 통신을 염두에 두고 만들어진 사양으로, 기존 3.0+HS과 방향성을 달리하여, 제품 제작자는 3.0+HS 및 4.0을 별도로 목적에 맞춰 채용하는 식이 되었다.

 
 블루투스 4.1

블루투스 SIG는 2013년 12월 블루투스 4.1의 새로운 기능을 발표했다[18]. 블루투스 4.1의 주요 특징은 다음과 같다.
공존성(coexistence) 향상- 블루투스와 LTE 무선이 서로 통신 상태를 조정해 가까운 대역폭으로 인한 간섭 현상을 줄여준다.
더 나은 연결(better connections)- 블루투스 연결 장치끼리의 거리가 증가해 잠시 연결이 끊어지게 되면, 4.1 블루투스 장치는 거리 내로 되돌아올 시 자동으로 재연결된다.
데이터 전송 개선(improved data transfer)- 블루투스를 사용하는 악세서리 장치(헬스 기구) 등과의 통신 전송 상태를 보다 효율적으로 개선하였다. 개발자에게 더 많은 유연성 제공(more flexibility to developers)- 앞으로 있을 웨어러블 기기 붐에 대비한 업데이트로, 블루투스 연결을 통해 웨어러블 기기가 스마트폰의 주변장치이자 동시에 다른 장치와의 허브 역할도 할 수 있게 해준다. 또한 장래를 위해 사물 인터넷(The Internet of Things)을 위한 새로운 IPV6 사용 표준도 들어가 있다.


프로파일에 대한 자세한 내용은 공식 가이드에서 소개하고 있습니다. 해당 주제에서는 다루지 않습니다.



3. 연결을 위한 블루투스


프로그래밍으로 구현하기에 앞서서 가장 원초적인 이야기이지만, 블루투스 장치가 2대는 있어야 합니다.

하나는 Server 역할을 하기도 하면서, Client 역할을 할 수 있는 장치가 있어야 되겠습니다.



 장치 모습(외형, 내부)


171124-present.pptx


장치를 살펴본 것처럼, 가장 중요한 건 디바이스가 블루투스를 지원하는지를 알아야 합니다.

블루투스 칩셋이 지원된다면, 연결 장치를 확인하게 될 것이고, 채널을 생성해야 합니다.

검색(Discovery) 등을 하게 될 것이며.

요약하면 다음과 같은 API 활동을 수행합니다.


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

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




4. 프로그래밍 구현 - 전략

프로그래밍 구현을 어떠한 순서로 하는지 소개합니다.


 <!-- 블루투스 권한 획득 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />

<!-- 안드로이드 마시멜로우 이상 버전 / 블루투스 탐색 권한(GPS 활성화)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

 1. 메니페스트 환경 설정을 한다. 

 마시멜로우 버전 이상부터는 Wifi+Bluetooth+GPS 칩이 연결되어 있는 것으로 보입니다.

 안드로이드 공식 메뉴얼과는 다르게 ACCESS_{}_LOCATION의 권한을 허용해줘야 합니다.

 private BluetoothAdapter mBluetoothAdapter;

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


 2. 블루투스 장치가 지원하는지 확인한다.

 장치가 지원되지 않는데 블루투스를 동작시켜봐야 의미가 없습니다.

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



 3. 블루투스 장치가 지원 된다면, 블루투스 어뎁터를 활성화하여 사용할 수 있도록 해야 합니다.

 mBluetoothAdapter.isEnabled()은 블루투스 어뎁터의 활성화 상태를 {True, False}로 반환합니다.

 private void checkBTPermissions(){

      // 안드로이드 SDK 버전이 LOLLIPOP보다 클 때,
     if(Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP){

        int permissionCheck = this.checkSelfPermission("Manifest.permission.ACCESS_FINE_LOCATION");
          permissionCheck += this.checkSelfPermission("Manifest.permission.ACCESS_COARSE_LOCATION");

        if (permissionCheck != 0) {
             this.requestPermissions( new String[] {  

            Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, 1001);

                //Any number}

       }else{
           Log.d(TAG, "checkBTPermissions: No need to check permissions. SDK version < LOLLIPOP.");
          Toast.makeText(getApplicationContext(), "SDK버전이 롤리팝 아래 버전으로 인해 권한을 상승할 수 없습니다."

          Toast.LENGTH_SHORT).show();
          finish();
      }
}

 4. 안드로이드 버전이 LOLLIPOP버전보다 클 때.

 메니페스토(ACCESS_FINE_LOCATION, ACCESS_COURSE_LOCATION) 권한을 획득합니다.

 롤리팝 이상 버전에서는 해당 코드를 넣어서 권한을 반드시 획득해야 합니다.


 (실제 구현에서는 이 다음에 위젯 등의 윤곽을 잡는 구현을 하게 됩니다.)

 

 (BluetoothClientService.java)

/**
* 과거에 페어링 되었던 블루투스 디바이스 목록을 가져온다.
* @return
*/
public Set<BluetoothDevice> getPairedDevices() {
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
return pairedDevices;
}
-------------------------------------------------------------------------------------------------------------------
 (MainActivity.java)
private void getPairedDevices() {
Set<BluetoothDevice> devices = mClient.getPairedDevices();
for (BluetoothDevice device : devices) {
addDeviceToArrayAdapter(device);
}
}

 5. 페어링된 장치 조회

 페어링에는 크게 종류가 두 가지로 구성됩니다.

 1. 페어링이 된 장치.

 2. 페어링이 되지 않았지만, 페어링 대상이 되는 장치



 



 페어링의 종류에는 크게 두 가지로 구성됩니다.

  (BluetoothClientService.java)

public class BluetoothClientService{
private OnScanListener mOnScanListener;
/**
* 주변의 새 블루투스 디바이스를 스캔한다.
* @param context
* @param OnScanListener 블루투스를 스캔 이벤트.
* @return
*/
public boolean scanDevices(Context context, onScanListener onScanListener) {

if(!mBluetoothAdapter.isEnabled()) return false;
if(mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
}
mOnScanListener = onScanListener;
IntentFilter filterFound = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filterFound.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filterFound.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
context.registerReceiver(mDiscoveryReceiver, filterFound);
mBluetoothAdapter.startDiscovery();
return true;
}

/**
* 스캔을 취소한다.
* @param context
*/
public void cancelScan(Context context) {
if(!mBluetoothAdapter.isEnabled() || !mBluetoothAdapter.isDiscovering()) return;
mBluetoothAdapter.cancelDiscovery();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
if(mOnScanListener != null) mOnScanListener.onFinish();
}
 // (핵심 영역)
 private BroadcastReceiver mDiscoveryReceiver = 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);
if(mOnScanListener != null) mOnScanListener.onFoundDevice(device);
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
if(mOnScanListener != null) mOnScanListener.onFinish();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
if(mOnScanListener != null) mOnScanListener.onStart();
}
}
};

// 인터페이스
public static interface OnScanListener {
public void onStart();
public void onFoundDevice(BluetoothDevice bluetoothDevice);
public void onFinish();
}

} // classes;

------------------------------------------------------------------------------------------------------
(MainActivity.java)

 private LinkedList<BluetoothDevice> mBluetoothDevices = new LinkedList<BluetoothDevice>();
 private ArrayAdapter<String> mDeviceArrayAdapter;
private ProgressDialog mLoadingDialog;
private AlertDialog mDeviceListDialog;

...........................


private void addDeviceToArrayAdapter(BluetoothDevice device) {
if (mBluetoothDevices.contains(device)) {
mBluetoothDevices.remove(device);
mDeviceArrayAdapter.remove(device.getName() + "\n" + device.getAddress());
}
mBluetoothDevices.add(device);
mDeviceArrayAdapter.add(device.getName() + "\n" + device.getAddress());
mDeviceArrayAdapter.notifyDataSetChanged();

}

 private void scanDevices() {
BluetoothClientService btSet = mClient;
btSet.scanDevices(getApplicationContext(), new BluetoothClientService.OnScanListener() {
String message = "";

@Override
public void onStart() {
Log.d(TAG, "Scan Start.");
mLoadingDialog.show();
message = "찾는중(Scanning...)";

// 프로그래스 - 다이얼
mLoadingDialog.setMessage("찾는중(Scanning...)");
mLoadingDialog.setCancelable(true);
mLoadingDialog.setCanceledOnTouchOutside(false);

mLoadingDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
BluetoothClientService btSet = mClient;
btSet.cancelScan(getApplicationContext());
}
});
}

@Override
public void onFoundDevice(BluetoothDevice bluetoothDevice) {
addDeviceToArrayAdapter(bluetoothDevice);
message += "\n" + bluetoothDevice.getName() + "\n" + bluetoothDevice.getAddress();
mLoadingDialog.setMessage(message);
}

@Override
public void onFinish() {
Log.d(TAG, "찾기 완료(Scan finish.)");
message = "";
mLoadingDialog.cancel();
mLoadingDialog.setCancelable(false);
mLoadingDialog.setOnCancelListener(null);
mDeviceListDialog.show();
}
});
}




 6. 신규 장치 찾기(수신)

 
(MainActivity.java)
private void initDeviceListDialog() {

mDeviceArrayAdapter = new ArrayAdapter<String>(getApplicationContext(), R.layout.device_items);
ListView listView = new ListView(getApplicationContext());
listView.setAdapter(mDeviceArrayAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String item = (String) parent.getItemAtPosition(position);
for (BluetoothDevice device : mBluetoothDevices) {
if (item.contains(device.getAddress())) {
connect(device);
mDeviceListDialog.cancel();
}
}
}
});

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("블루투스 장치 선택(Select bluetooth device)");
builder.setView(listView);
builder.setPositiveButton("찾기(Scan)", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
scanDevices();
}
});
builder.setNegativeButton("취소(Cancel)", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mClient.cancelScan(getApplicationContext());
dialog.dismiss();
}
});

mDeviceListDialog = builder.create();
mDeviceListDialog.setCanceledOnTouchOutside(false);
}


private void connect(BluetoothDevice device) {

// 구현
}


} // end of classes

 7. 페어링 장치 - 화면 구현(5, 6번)

 - 여기까지 페어링에 관한 이야기입니다.

 초기 페어링에 관한 이야기입니다. 연결에 대해서 소개합니다.

 (BluetoothClientService.java)

public class BluetoothClientService{

private BluetoothSocket mBluetoothSocket;

private UUID mUUID = UUID.fromString(SERIAL_UUID);
private AtomicBoolean mIsConnection = new AtomicBoolean(false);

private ExecutorService mReadExecutor;
private ExecutorService mWriteExecutor;

private BluetoothStreamingHandler mBluetoothStreamingHandler;
private Handler mMainHandler = new Handler(Looper.getMainLooper());
/**
* 블루투스 디바이스와 시리얼로 연결한다.
* @param context
* @param device 블루투스 디바이스. {@link getPairedDevices} 또는 {@link scanDevices} 를 통하여 가져온
   블루투스 디바이스 인스턴스.
* @param bluetoothStreamingHandler 블루투스 스트리밍 핸들러.
* @return 만약 블루투스를 사용할 수 없는 상태라면 false. {@link enableBluetooth} 를 통하여 블루투스를
사용 가능한 상태로 만들어줘야 한다.
*/
public boolean connect(final Context context,final BluetoothDevice device,
  final BluetoothStreamingHandler bluetoothStreamingHandler) {
if(!isEnabled()) return false;
mConnectedDevice = device;
mBluetoothStreamingHandler = bluetoothStreamingHandler;
if(isConnection()) {
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mIsConnection.set(false);
mBluetoothSocket.close();
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
connect(context, device, bluetoothStreamingHandler);
}
});
} else {
mIsConnection.set(true);
connectClient();
}
return true;
}

private void connectClient() {
try {
mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);
} catch (IOException e) {
close();
e.printStackTrace();
mBluetoothStreamingHandler.onError(e);
return;
}
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mBluetoothAdapter.cancelDiscovery();
mBluetoothSocket.connect();
manageConnectedSocket(mBluetoothSocket);
callConnectedHandlerEvent();
mReadExecutor.execute(mReadRunnable);
} catch (final IOException e) {
close();
e.printStackTrace();
mMainHandler.post(new Runnable() {
@Override
public void run() {
mBluetoothStreamingHandler.onError(e);
}
});
mIsConnection.set(false);
try {
mBluetoothSocket.close();
} catch (Exception ec) {
ec.printStackTrace();
}
}
}
});
}


private void manageConnectedSocket(BluetoothSocket socket) throws IOException {
mInputStream = socket.getInputStream();
mOutputStream = socket.getOutputStream();
}

private void callConnectedHandlerEvent() {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mBluetoothStreamingHandler.onConnected();
}
});
}


private boolean write(final byte[] buffer) {
if(!mIsConnection.get()) return false;
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mOutputStream.write(buffer);
} catch (Exception e) {
close();
e.printStackTrace();
mBluetoothStreamingHandler.onError(e);
}
}
});
return true;
}


private boolean close() {
mConnectedDevice = null;
if(mIsConnection.get()) {
mIsConnection.set(false);
try {
mBluetoothSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
mMainHandler.post(mCloseRunnable);
return true;
}
return false;
}

private Runnable mCloseRunnable = new Runnable() {
@Override
public void run() {
if(mBluetoothStreamingHandler != null) {
mBluetoothStreamingHandler.onDisconnected();
}
}
};


private Runnable mCloseRunnable = new Runnable() {
@Override
public void run() {
if(mBluetoothStreamingHandler != null) {
mBluetoothStreamingHandler.onDisconnected();
}
}
};

private Runnable mReadRunnable = new Runnable() {
@Override
public void run() {
try {
final byte[] buffer = new byte[256];
final int readBytes = mInputStream.read(buffer);
mMainHandler.post(new Runnable() {
@Override
public void run() {
if(mBluetoothStreamingHandler != null) {
mBluetoothStreamingHandler.onData(buffer ,readBytes);
}
}
});
mReadExecutor.execute(mReadRunnable);
} catch (Exception e) {
close();
e.printStackTrace();
}
}
};

// 추상 클래스
public abstract static class BluetoothStreamingHandler {
public abstract void onError(Exception e);
public abstract void onConnected();
public abstract void onDisconnected();
public abstract void onData(byte[] buffer, int length);
public final boolean close() {
BluetoothClientService btSet = getInstance();
if(btSet != null)
return btSet.close();
return false;
}
public final boolean write(byte[] buffer) {
BluetoothClientService btSet = getInstance();
if(btSet != null)
return btSet.write(buffer);
return false;
}
}

} // end of classes;

----------------------------------------------------------------------------------------------

 (MainActivity.java)

private void connect(BluetoothDevice device) {
mLoadingDialog.setMessage("연결중(Connecting...)");
mLoadingDialog.setCancelable(false);
mLoadingDialog.show();

BluetoothClientService btSet = mClient;
btSet.connect(getApplicationContext(), device, mBTHandler);
}
private void addText(String text) {
mTextView.append(text);
final int scrollAmount = mTextView.getLayout().getLineTop(mTextView.getLineCount()) - mTextView.getHeight();
if (scrollAmount > 0)
mTextView.scrollTo(0, scrollAmount);
else
mTextView.scrollTo(0, 0);
}

private BluetoothClientService.BluetoothStreamingHandler mBTHandler = new BluetoothClientService.BluetoothStreamingHandler() {
ByteBuffer mmByteBuffer = ByteBuffer.allocate(1024);

@Override
public void onError(Exception e) {
mLoadingDialog.cancel();
addText("Message : Connection error - " + e.toString() + "\n");
//mMenu.getItem(0).setTitle(R.string.action_connect);
}

@Override
public void onDisconnected() {
//mMenu.getItem(0).setTitle(R.string.action_connect);
mLoadingDialog.cancel();
addText("Message : Disconnected.\n");
}

@Override
public void onData(byte[] buffer, int length) {
if (length == 0) return;
if (mmByteBuffer.position() + length >= mmByteBuffer.capacity()) {
ByteBuffer newBuffer = ByteBuffer.allocate(mmByteBuffer.capacity() * 2);
newBuffer.put(mmByteBuffer.array(), 0, mmByteBuffer.position());
mmByteBuffer = newBuffer;
}
mmByteBuffer.put(buffer, 0, length);
if (buffer[length - 1] == '\0') {
addText(mClient.getConnectedDevice().getName() + " : " +
new String(mmByteBuffer.array(), 0, mmByteBuffer.position()) + '\n');
mmByteBuffer.clear();
}
}

@Override
public void onConnected() {
addText("Message : Connected. " + mClient.getConnectedDevice().getName() + "\n");
mLoadingDialog.cancel();
//mMenu.getItem(0).setTitle(R.string.action_disconnect);
}
};


 9. 연결에 관한 내용(연결, 메시지 송 수신에 대한 내용)

 

 public class BluetoothClientService {

public static final int REQUEST_ENABLE_BT = 1;

static final private String SERIAL_UUID = "00001101-0000-1000-8000-00805F9B34FB";

private BluetoothAdapter mBluetoothAdapter;
private OnScanListener mOnScanListener;
private BluetoothSocket mBluetoothSocket;

private UUID mUUID = UUID.fromString(SERIAL_UUID);
private AtomicBoolean mIsConnection = new AtomicBoolean(false);

private ExecutorService mReadExecutor;
private ExecutorService mWriteExecutor;

private BluetoothStreamingHandler mBluetoothStreamingHandler;
private Handler mMainHandler = new Handler(Looper.getMainLooper());

private BluetoothDevice mConnectedDevice = null;
private InputStream mInputStream;
private OutputStream mOutputStream;

static private BluetoothClientService sThis = null;


private BluetoothClientService() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mReadExecutor = Executors.newSingleThreadExecutor();
mWriteExecutor = Executors.newSingleThreadExecutor();
}

/**
* 연결을 닫고 자원을 해지한다.
* 앱 종료시 반드시 호출해 줘야 한다.
*/
public void clear() {
close();
mReadExecutor.shutdownNow();
mWriteExecutor.shutdownNow();
sThis = null;
}

 10. 생성자 등 구현




@Override
public boolean onOptionsItemSelected(MenuItem item) {

switch ( item.getItemId() ){

case 1:
enableBluetooth();
return true;

case 2:
pairDialog();
return true;

case 3:
discoveryDialog();
return true;

case 4:
showCodeDlg();
return true;

default:
return super.onOptionsItemSelected(item);
}

}

private void pairDialog() {
boolean connect = mClient.isConnection();

if (!connect && mClient.isEnabled()) {
getPairedDevices();
mDeviceListDialog.show();
} else {

Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
mBTHandler.close();
}
}

private void discoveryDialog(){

if( mClient.isEnabled()) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
}
else
{
Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
}
}

private void showCodeDlg() {

TextView codeView = new TextView(this);
codeView.setText( Html.fromHtml(readCode()) );
codeView.setMovementMethod(new ScrollingMovementMethod());
codeView.setBackgroundColor(Color.parseColor("#202020"));

new AlertDialog.Builder(this, android.R.style.Theme_Holo_Light_DialogWhenLarge)
.setView(codeView)
.setPositiveButton("OK", new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
}).show();
}

private String readCode() {

try {
InputStream is = getAssets().open("HC_06_Echo.txt");
int length = is.available();
byte[] buffer = new byte[length];
is.read(buffer);
is.close();
String code = new String(buffer);
buffer = null;
return code;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}


private void checkBTEnabled(){

if ( mClient.isEnabled() ) {
initDeviceListDialog();
initWidget();
}
}


(윤곽 등을 잡는 구현의 예)



5. 공식 가이드에서 소개하는 내용(번외)

1. https://developer.android.com/guide/topics/connectivity/bluetooth.html

2. https://developer.android.com/samples/BluetoothChat/index.html


블루투스 공식 가이드에서 소개하고 있는 것은 BluetoothChat 프로젝트를 예제로서 소개하고 있습니다.

레퍼런스 읽는 것도 무척 중요합니다. "블루투스" 채팅 프로젝트(공식사이트 제공)를 아마 안드로이드 스튜디오로 동작시켜본다면 무척 소스코드가 복잡한 것은 물론이고 동작하지 않습니다.

예전 디바이스 기종으로 한다면, 동작할지도 모릅니다. (최신 안드로이드 스튜디오로 확인해본 결과)

- 빌드가 안 된다는 것이 아니라, 연결부터 채팅 동작이 안 되는 현상을 관찰할 수 있음.

- 아마 블루투스 레퍼런스를 선호하는 분들이 대체적으로 졸업작품 등을 준비하거나 하는 분들이 선호할 것으로 보입니다.

가이드에 있는 내용을 소개합니다. 해설도 넣었습니다.

앞서 소개된 내용을 소개합니다.



* 블루투스 권한


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

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


* 블루투스 설정

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

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


1.BluetoothAdapter 가져오기
모든 블루투스 액티비티를 위해 BluetoothAdapter가 필요합니다. BluetoothAdapter를 가져오려면 정적 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);
}


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

startActivityForResult()로 전달된 REQUEST_ENABLE_BT 상수는 시스템이 requestCode 매개변수로서 onActivityResult() 구현에서 개발자에게 다시 전달하는 지역적으로 정의된 정수(0보다 커야 함)입니다.

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


protected void onActivityResult( int requestCode, int resultCode, Intent data ){

        // startAcitivityForResult로 넘긴 값

        // -> requestCode가 넘어온다는 이야기.

}


3. 기기 찾기

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

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


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


중요한 부분이어서 색깔팬으로 표기를 하였습니다.


3-1. 페어링된 기기 쿼리

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());
   
}
}

 페어링이 되어진 기기를 검색합니다.


3-2. 기기 검색

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

애플리케이션은 검색된 각 기기에 대한 정보를 수신하기 위해 ACTION_FOUND 인텐트에 대한 BroadcastReceiver를 등록해야 합니다. 시스템은 각 기기에 대해 ACTION_FOUND 인텐트를 브로드캐스트합니다. 이 인텐트는 BluetoothDevice 및 BluetoothClass을 각각 포함하는 추가 필드 EXTRA_DEVICE 및 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

 기기 검색


연결을 시작하기 위해 BluetoothDevice 객체에서 필요한 것은 MAC 주소입니다. 이 예시에서 MAC 주소는 사용자에게 표시되는 ArrayAdapter의 일부로 저장됩니다. MAC 주소는 연결을 시작하도록 나중에 추출할 수 있습니다. 연결 생성에 대한 자세한 내용은 기기 연결 섹션을 참조하세요.


주의: 기기 검색은 리소스 사용량이 많은 블루투스 어댑터 프로시저입니다. 연결할 기기를 검색한 경우 연결을 시도하기 전에 항상 cancelDiscovery()를 사용하여 검색을 중단해야 합니다. 또한 이미 기기와 연결 중인 경우 검색은 연결에 사용 가능한 대역폭을 상당히 줄일 수 있으므로 연결 중에 검색을 해서는 안 됩니다.


4. 검색 기능 활성화

로컬 기기를 다른 기기가 검색할 수 있게 하려면 ACTION_REQUEST_DISCOVERABLE 작업 인텐트를 사용하여 startActivityForResult(Intent, int)을 호출합니다. 그러면 (애플리케이션을 중지하지 않고) 시스템 설정을 통한 검색 가능 모드 활성화 요청이 발급됩니다. 기본적으로 기기가 120초 동안 검색 가능하게 됩니다. EXTRA_DISCOVERABLE_DURATION 인텐트 엑스트라를 추가하여 다른 기간을 정의할 수 있습니다. 앱이 설정할 수 있는 최대 기간은 3600초이며 값이 0인 경우 기기가 항상 검색 가능합니다. 0 미만 또는 3600 초과 값은 120초로 자동 설정됩니다. 예를 들어, 다음 스니펫은 기간을 300으로 설정했습니다.


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




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


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


기기는 할당된 시간 동안 검색 가능 모드를 유지합니다. 검색 가능 모드 변경 알림을 받으려면 ACTION_SCAN_MODE_CHANGED 인텐트에 대해 BroadcastReceiver를 등록할 수 있습니다. 이는 새로운 스캔 모드와 이전 스캔 모드를 각각 알려주는 추가 필드 EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE를 포함합니다. 각각에 대해 가능한 값은 SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE 또는 SCAN_MODE_NONE이며, 각 값은 기기가 검색 모드이거나, 검색 모드는 아니지만 연결을 수신할 수 있거나, 검색 모드도 아니고 연결도 수신할 수 없음을 나타냅니다.

원격 기기에 대한 연결을 시작하려면 기기 검색 기능을 활성화할 필요가 없습니다. 원격 기기는 연결을 시작하기 전에 기기를 검색할 수 있어야 하므로 들어오는 연결을 수락하는 서버 소켓을 애플리케이션이 호스팅하려는 경우에만 검색 기능 활성화가 필요합니다.


5. 기기 연결

매우 중요합니다. PIN 코드 등에 관한 문제부터 시작해서 소켓을 생성하는데 필요한 정보 등을 가지고 있습니다.


문제는 이렇게 창을 띄울 수 있냐는 겁니다. 아마도 가이드에는 제시되어 있지 않습니다.


private void connectClient() {
try {
mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);


// mConnectedDevice.{setPin??}

// ......등.....(BluetoothDevice mConnectedDevice가 가지고 있는 메서드 등을 참고하기 바람.)


mBluetoothAdapter.cancelDiscovery();
mBluetoothSocket.connect();

소켓을 열때, PIN 창이 뜰 것으로 보입니다.

기기연결에 필요한 구성요소를 크게 3가지로 추려보면,

ConnectThread,

AcceptThread,

ConnectedThread 3가지로 살펴볼 수 있습니다.

그런데, 정작 앞서 예제에서는 3가지 쓰레드로 구현하지 않았습니다.

private void connectClient() {


     try {
      mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);
   } catch (IOException e) {
       close();
       e.printStackTrace();
   mBluetoothStreamingHandler.onError(e);
   return;
   }
   

   mWriteExecutor.execute(new Runnable() {
              @Override
           public void run() {
               try {
                  mBluetoothAdapter.cancelDiscovery();
                     mBluetoothSocket.connect();
                      manageConnectedSocket(mBluetoothSocket);
                      callConnectedHandlerEvent();
                  mReadExecutor.execute(mReadRunnable);
                 } catch (final IOException e) {
                      close();
                      e.printStackTrace();
                     mMainHandler.post(new Runnable() {
                             @Override
                        public void run() {
                             mBluetoothStreamingHandler.onError(e);
                              }
             });
           mIsConnection.set(false);
                

           try {
              mBluetoothSocket.close();
             } catch (Exception ec) {
                  ec.printStackTrace();
             }
        }
     }
});



가이드에서 제시하는 코드를 살펴보면.


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

 1. 블루투스 연결을 시작하는 스레드의 기본적인 예입니다.

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

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

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

 3. 연결 관리


bluetoothChat 소스코드의 살펴보면,


(BluetoothChatService.java)

/**
 * This class does all the work for setting up and managing Bluetooth
 * connections with other devices. It has a thread that listens for
 * incoming connections, a thread for connecting with a device, and a
 * thread for performing data transmissions when connected.
 */
public class BluetoothChatService {
    // Debugging
    private static final String TAG = "BluetoothChatService";

    // Name for the SDP record when creating server socket
    private static final String NAME_SECURE = "BluetoothChatSecure";
    private static final String NAME_INSECURE = "BluetoothChatInsecure";

    // Unique UUID for this application
    private static final UUID MY_UUID_SECURE =
            UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
    private static final UUID MY_UUID_INSECURE =
            UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66");

   

    // Member fields
    private final BluetoothAdapter mAdapter;
    private final Handler mHandler;
    private AcceptThread mSecureAcceptThread;
    private AcceptThread mInsecureAcceptThread;
    private ConnectThread mConnectThread;
    private ConnectedThread mConnectedThread;
    private int mState;
    private int mNewState;


    // Constants that indicate the current connection state
    public static final int STATE_NONE = 0;       // we're doing nothing
    public static final int STATE_LISTEN = 1;     // now listening for incoming connections
    public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
    public static final int STATE_CONNECTED = 3;  // now connected to a remote device


    /**
     * Constructor. Prepares a new BluetoothChat session.
     *
     * @param context The UI Activity Context
     * @param handler A Handler to send messages back to the UI Activity
     */
    public BluetoothChatService(Context context, Handler handler) {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mState = STATE_NONE;
        mNewState = mState;
        mHandler = handler;
    }


   .......

    /**
     * Start the ConnectThread to initiate a connection to a remote device.
     *
     * @param device The BluetoothDevice to connect
     * @param secure Socket Security type - Secure (true) , Insecure (false)
     */
    public synchronized void connect(BluetoothDevice device, boolean secure) {
        Log.d(TAG, "connect to: " + device);

        // Cancel any thread attempting to make a connection
        if (mState == STATE_CONNECTING) {
            if (mConnectThread != null) {
                mConnectThread.cancel(); // 연결 끊기
                mConnectThread = null;
            }
        }

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {
            mConnectedThread.cancel(); // 연결 끊기
            mConnectedThread = null;
        }

        // Start the thread to connect with the given device
        mConnectThread = new ConnectThread(device, secure);
        mConnectThread.start();
        // Update UI title
        updateUserInterfaceTitle();
    }


     * Start the ConnectedThread to begin managing a Bluetooth connection
     *
     * @param socket The BluetoothSocket on which the connection was made
     * @param device The BluetoothDevice that has been connected
     */
    public synchronized void connected(BluetoothSocket socket, BluetoothDevice
            device, final String socketType) {
        Log.d(TAG, "connected, Socket Type:" + socketType);

        // Cancel the thread that completed the connection
        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        // Cancel any thread currently running a connection
        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        // Cancel the accept thread because we only want to connect to one device
        if (mSecureAcceptThread != null) {
            mSecureAcceptThread.cancel();
            mSecureAcceptThread = null;
        }
        if (mInsecureAcceptThread != null) {
            mInsecureAcceptThread.cancel();
            mInsecureAcceptThread = null;
        }

        // Start the thread to manage the connection and perform transmissions
        mConnectedThread = new ConnectedThread(socket, socketType);
        mConnectedThread.start();

        // Send the name of the connected device back to the UI Activity
        Message msg = mHandler.obtainMessage(Constants.MESSAGE_DEVICE_NAME);
        Bundle bundle = new Bundle();
        bundle.putString(Constants.DEVICE_NAME, device.getName());
        msg.setData(bundle);
        mHandler.sendMessage(msg);
        // Update UI title
        updateUserInterfaceTitle();
    }

    /**
     * Stop all threads
     */
    public synchronized void stop() {
        Log.d(TAG, "stop");

        if (mConnectThread != null) {
            mConnectThread.cancel();
            mConnectThread = null;
        }

        if (mConnectedThread != null) {
            mConnectedThread.cancel();
            mConnectedThread = null;
        }

        if (mSecureAcceptThread != null) {
            mSecureAcceptThread.cancel();
            mSecureAcceptThread = null;
        }

        if (mInsecureAcceptThread != null) {
            mInsecureAcceptThread.cancel();
            mInsecureAcceptThread = null;
        }
        mState = STATE_NONE;
        // Update UI title
        updateUserInterfaceTitle();
    }

    /**
     * Write to the ConnectedThread in an unsynchronized manner
     *
     * @param out The bytes to write
     * @see ConnectedThread#write(byte[])
     */
    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);
    }

    /**
     * Indicate that the connection attempt failed and notify the UI Activity.
     */
    private void connectionFailed() {
        // Send a failure message back to the Activity
        Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
        Bundle bundle = new Bundle();
        bundle.putString(Constants.TOAST, "Unable to connect device");
        msg.setData(bundle);
        mHandler.sendMessage(msg);

        mState = STATE_NONE;
        // Update UI title
        updateUserInterfaceTitle();

        // Start the service over to restart listening mode
        BluetoothChatService.this.start();
    }

    /**
     * Indicate that the connection was lost and notify the UI Activity.
     */
    private void connectionLost() {
        // Send a failure message back to the Activity
        Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
        Bundle bundle = new Bundle();
        bundle.putString(Constants.TOAST, "Device connection was lost");
        msg.setData(bundle);
        mHandler.sendMessage(msg);

        mState = STATE_NONE;
        // Update UI title
        updateUserInterfaceTitle();

        // Start the service over to restart listening mode
        BluetoothChatService.this.start();
    }


    /**
     * This thread runs while listening for incoming connections. It behaves
     * like a server-side client. It runs until a connection is accepted
     * (or until cancelled).
     */
    private class AcceptThread extends Thread {
        // The local server socket
        private final BluetoothServerSocket mmServerSocket;
        private String mSocketType;

        public AcceptThread(boolean secure) {
            BluetoothServerSocket tmp = null;
            mSocketType = secure ? "Secure" : "Insecure";

            // Create a new listening server socket
            try {
                if (secure) {
                    tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
                            MY_UUID_SECURE);
                } else {
                    tmp = mAdapter.listenUsingInsecureRfcommWithServiceRecord(
                            NAME_INSECURE, MY_UUID_INSECURE);
                }
            } catch (IOException e) {
                Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e);
            }
            mmServerSocket = tmp;
            mState = STATE_LISTEN;
        }

        public void run() {
            Log.d(TAG, "Socket Type: " + mSocketType +
                    "BEGIN mAcceptThread" + this);
            setName("AcceptThread" + mSocketType);

            BluetoothSocket socket = null;

            // Listen to the server socket if we're not connected
            while (mState != STATE_CONNECTED) {
                try {
                    // This is a blocking call and will only return on a
                    // successful connection or an exception
                    socket = mmServerSocket.accept();
                } catch (IOException e) {
                    Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
                    break;
                }

                // If a connection was accepted
                if (socket != null) {
                    synchronized (BluetoothChatService.this) {
                        switch (mState) {
                            case STATE_LISTEN:
                            case STATE_CONNECTING:
                                // Situation normal. Start the connected thread.
                                connected(socket, socket.getRemoteDevice(),
                                        mSocketType);
                                break;
                            case STATE_NONE:
                            case STATE_CONNECTED:
                                // Either not ready or already connected. Terminate new socket.
                                try {
                                    socket.close();
                                } catch (IOException e) {
                                    Log.e(TAG, "Could not close unwanted socket", e);
                                }
                                break;
                        }
                    }
                }
            }
            Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType);

        }

        public void cancel() {
            Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
            try {
                mmServerSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
            }
        }
    }


    /**
     * This thread runs while attempting to make an outgoing connection
     * with a device. It runs straight through; the connection either
     * succeeds or fails.
     */
    private class ConnectThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final BluetoothDevice mmDevice;
        private String mSocketType;


        public ConnectThread(BluetoothDevice device, boolean secure) {
            mmDevice = device;
            BluetoothSocket tmp = null;
            mSocketType = secure ? "Secure" : "Insecure";

            // Get a BluetoothSocket for a connection with the
            // given BluetoothDevice
            try {
                if (secure) {
                    tmp = device.createRfcommSocketToServiceRecord(
                            MY_UUID_SECURE);

                } else {
                    tmp = device.createInsecureRfcommSocketToServiceRecord(
                            MY_UUID_INSECURE);

                }
            } catch (IOException e) {
                Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
            }
            mmSocket = tmp;
            mState = STATE_CONNECTING;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
            setName("ConnectThread" + mSocketType);

            // Always cancel discovery because it will slow down a connection
          mAdapter.cancelDiscovery();

            // Make a connection to the BluetoothSocket
            try {
                // This is a blocking call and will only return on a
                // successful connection or an exception
                mmSocket.connect();
            } catch (IOException e) {
                // Close the socket
                try {
                    mmSocket.close();
                } catch (IOException e2) {
                    Log.e(TAG, "unable to close() " + mSocketType +
                            " socket during connection failure", e2);
                }
                connectionFailed();
                return;
            }

            // Reset the ConnectThread because we're done
            synchronized (BluetoothChatService.this) {
                mConnectThread = null;
            }

            // Start the connected thread
            connected(mmSocket, mmDevice, mSocketType);
        }

        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
            }
        }
    }


    /**
     * This thread runs during a connection with a remote device.
     * It handles all incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final InputStream mmInStream;
       private final OutputStream mmOutStream;


        public ConnectedThread(BluetoothSocket socket, String socketType) {
            Log.d(TAG, "create ConnectedThread: " + socketType);
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            // Get the BluetoothSocket input and output streams
            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
            mState = STATE_CONNECTED;
        }

        public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            byte[] buffer = new byte[1024];
            int bytes;

            // Keep listening to the InputStream while connected
            while (mState == STATE_CONNECTED) {
                try {
                    // Read from the InputStream
                    bytes = mmInStream.read(buffer);

                    // Send the obtained bytes to the UI Activity
                    mHandler.obtainMessage(Constants.MESSAGE_READ, bytes, -1, buffer)
                            .sendToTarget();
                } 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);

                // Share the sent message back to the UI Activity
                mHandler.obtainMessage(Constants.MESSAGE_WRITE, -1, -1, buffer)
                        .sendToTarget();
            } 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);
            }
        }
    }
}

..........................................................

(BluetoothChatFragment.java)


public class BluetoothChatFragment extends Fragment {

    private static final int REQUEST_ENABLE_BT = 3;

    // Intent request codes
    private static final int REQUEST_CONNECT_DEVICE_SECURE = 1;
    private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2;
    private static final int REQUEST_ENABLE_BT = 3;


    // Layout Views
    private ListView mConversationView;
    private EditText mOutEditText;
    private Button mSendButton;

    /**
     * Name of the connected device
     */
    private String mConnectedDeviceName = null;

    /**
     * Array adapter for the conversation thread
     */
    private ArrayAdapter<String> mConversationArrayAdapter;

    /**
     * String buffer for outgoing messages
     */
    private StringBuffer mOutStringBuffer;

    /**
     * Local Bluetooth adapter
     */
    private BluetoothAdapter mBluetoothAdapter = null;

    /**
     * Member object for the chat services
     */
    private BluetoothChatService mChatService = null;


     ...........................

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        mConversationView = (ListView) view.findViewById(R.id.in);
        mOutEditText = (EditText) view.findViewById(R.id.edit_text_out);
        mSendButton = (Button) view.findViewById(R.id.button_send);
    }
    

    private void setupChat() {
        Log.d(TAG, "setupChat()");


        // Initialize the array adapter for the conversation thread
        mConversationArrayAdapter = new ArrayAdapter<String>(getActivity(), R.layout.message);

        mConversationView.setAdapter(mConversationArrayAdapter);


        // Initialize the compose field with a listener for the return key
        mOutEditText.setOnEditorActionListener(mWriteListener);


        // Initialize the send button with a listener that for click events
        mSendButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                // Send a message using content of the edit text widget
                View view = getView();
                if (null != view) {
                    TextView textView = (TextView) view.findViewById(R.id.edit_text_out);
                    String message = textView.getText().toString();
                    sendMessage(message);
                }
            }
        });


        // Initialize the BluetoothChatService to perform bluetooth connections
        mChatService = new BluetoothChatService(getActivity(), mHandler);

        // Initialize the buffer for outgoing messages
        mOutStringBuffer = new StringBuffer("");
    }

 

    .......

    .......


        switch (requestCode) {
            case REQUEST_CONNECT_DEVICE_SECURE:
                // When DeviceListActivity returns with a device to connect
                if (resultCode == Activity.RESULT_OK) {
                    connectDevice(data, true);
                }
                break;
            case REQUEST_CONNECT_DEVICE_INSECURE:
                // When DeviceListActivity returns with a device to connect
                if (resultCode == Activity.RESULT_OK) {
                    connectDevice(data, false);
                }
                break;
            case REQUEST_ENABLE_BT:
                // When the request to enable Bluetooth returns
                if (resultCode == Activity.RESULT_OK) {
                    // Bluetooth is now enabled, so set up a chat session
                    setupChat();
                } else {
                    // User did not enable Bluetooth or an error occurred
                    Log.d(TAG, "BT not enabled");
                    Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving,
                            Toast.LENGTH_SHORT).show();
                    getActivity().finish();
                }
        }
    }

    /**
     * Establish connection with other device
     *
     * @param data   An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra.
     * @param secure Socket Security type - Secure (true) , Insecure (false)
     */
    private void connectDevice(Intent data, boolean secure) {
        // Get the device MAC address
        String address = data.getExtras()
                .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
        // Get the BluetoothDevice object
        BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
        // Attempt to connect to the device
        mChatService.connect(device, secure);
    }
   ...... // 일부분.


중요한 부분을 주로 압축해서 올려봤습니다.

그러나 얻을 수 있는 부분은 연결에 관한 secure모드 등에 대한 명세 등 살펴볼 수 있습니다.

동작을 해보면, 채팅 기능이 되진 않을 것입니다.

다만, BluetoothChat 프로젝트에서는 블루투스 연결에 관한 중요한 이야기를 다루고 있습니다.



6. 블루투스 채팅 - 구현/동작 영상


 


 동작 영상1)





 동작 영상2)




7. 소스코드



<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.localhost.kr.bluetoothchat">

<!-- 블루투스 권한 획득 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />

<!-- 안드로이드 마시멜로우 이상 버전 / 블루투스 탐색 권한(GPS 활성화)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>


 AndroidManifest.xml




<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.localhost.kr.bluetoothchat.MainActivity">

<LinearLayout android:layout_height="match_parent" android:layout_width="match_parent"
        android:orientation="vertical">
<TextView android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:id="@+id/textViewTerminal"
android:textSize="12sp"
android:scrollbars="vertical"
android:background="#fdfdff"
android:layout_weight="1"/>


<LinearLayout android:layout_height="wrap_content"
android:layout_width="match_parent">
<EditText android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:id="@+id/editTextInput"
android:layout_weight="1"
android:inputType="textPersonName"
android:ems="10">
<requestFocus/></EditText>

<Button android:layout_height="wrap_content"
android:layout_width="80dp"
android:id="@+id/buttonSend"
android:text="Send"
style="?android:attr/buttonStyleSmall"/>

</LinearLayout>

</LinearLayout>

</LinearLayout>


 activity_main.xml

 <?xml version="1.0" encoding="utf-8"?>
<TextView android:textSize="18sp"
       android:textColor="#000000"
       android:padding="5dp"
       android:layout_height="wrap_content"
       android:layout_width="match_parent"
       xmlns:android="http://schemas.android.com/apk/res/android"/>

 device_items.xml




package com.localhost.kr.bluetoothchat;

import android.Manifest;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Html;
import android.text.method.ScrollingMovementMethod;
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.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Set;

public class MainActivity extends AppCompatActivity {

private final String TAG = "MainActivity";

private LinkedList<BluetoothDevice> mBluetoothDevices = new LinkedList<BluetoothDevice>();
private ArrayAdapter<String> mDeviceArrayAdapter;

private BluetoothClientService mClient;
private ProgressDialog mLoadingDialog;
private AlertDialog mDeviceListDialog;
private TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mClient = BluetoothClientService.getInstance();

Log.d(TAG, onCreate()");

if (mClient == null) {
Toast.makeText(getApplicationContext(), "Cannot use the Bluetooth device.", Toast.LENGTH_SHORT).show();
finish();
}

Log.d(TAG, onCreate2()");
checkBTPermissions();
initWidget();
initDeviceListDialog();
}

@Override
protected void onPause() {
Log.d(TAG, onPause()");

if ( mClient.isEnabled() )
mClient.cancelScan(getApplicationContext());

super.onPause();
}

@Override
protected void onResume() {
Log.d(TAG, onResume()");
super.onResume();
}

private void initWidget() {

mTextView = (TextView) findViewById(R.id.textViewTerminal); // 문자 터미널
mTextView.setMovementMethod(new ScrollingMovementMethod());
final EditText mEditTextInput = (EditText) findViewById(R.id.editTextInput);
final Button mButtonSend = (Button) findViewById(R.id.buttonSend);

mButtonSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendStringData(mEditTextInput.getText().toString());
mEditTextInput.setText("");
}
});

// 프로그래스 다이얼 로그 생성
mLoadingDialog = new ProgressDialog(this);
mLoadingDialog.setCancelable(false);

}

private void enableBluetooth() {

BluetoothClientService btSet = mClient;

if ( !mClient.isEnabled() ){
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, BluetoothClientService.REQUEST_ENABLE_BT);
return;
}

}

private void initDeviceListDialog() {

mDeviceArrayAdapter = new ArrayAdapter<String>(getApplicationContext(), R.layout.device_items);
ListView listView = new ListView(getApplicationContext());
listView.setAdapter(mDeviceArrayAdapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String item = (String) parent.getItemAtPosition(position);
for (BluetoothDevice device : mBluetoothDevices) {
if (item.contains(device.getAddress())) {
connect(device);
mDeviceListDialog.cancel();
}
}
}
});

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("블루투스 장치 선택(Select bluetooth device)");
builder.setView(listView);
builder.setPositiveButton("찾기(Scan)", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
scanDevices();
}
});
builder.setNegativeButton("취소(Cancel)", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mClient.cancelScan(getApplicationContext());
dialog.dismiss();
}
});

mDeviceListDialog = builder.create();
mDeviceListDialog.setCanceledOnTouchOutside(false);
}

private void addDeviceToArrayAdapter(BluetoothDevice device) {
if (mBluetoothDevices.contains(device)) {
mBluetoothDevices.remove(device);
mDeviceArrayAdapter.remove(device.getName() + "\n" + device.getAddress());
}
mBluetoothDevices.add(device);
mDeviceArrayAdapter.add(device.getName() + "\n" + device.getAddress());
mDeviceArrayAdapter.notifyDataSetChanged();

}

private void addText(String text) {
mTextView.append(text);
final int scrollAmount = mTextView.getLayout().getLineTop(mTextView.getLineCount()) - mTextView.getHeight();
if (scrollAmount > 0)
mTextView.scrollTo(0, scrollAmount);
else
mTextView.scrollTo(0, 0);
}


private void getPairedDevices() {
Set<BluetoothDevice> devices = mClient.getPairedDevices();
for (BluetoothDevice device : devices) {
addDeviceToArrayAdapter(device);
}
}

private void scanDevices() {
BluetoothClientService btSet = mClient;
btSet.scanDevices(getApplicationContext(), new BluetoothClientService.OnScanListener() {
String message = "";

@Override
public void onStart() {
Log.d(TAG, "Scan Start.");
mLoadingDialog.show();
message = "찾는중(Scanning...)";

// 프로그래스 - 다이얼
mLoadingDialog.setMessage("찾는중(Scanning...)");
mLoadingDialog.setCancelable(true);
mLoadingDialog.setCanceledOnTouchOutside(false);

mLoadingDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
BluetoothClientService btSet = mClient;
btSet.cancelScan(getApplicationContext());
}
});
}

@Override
public void onFoundDevice(BluetoothDevice bluetoothDevice) {
addDeviceToArrayAdapter(bluetoothDevice);
message += "\n" + bluetoothDevice.getName() + "\n" + bluetoothDevice.getAddress();
mLoadingDialog.setMessage(message);
}

@Override
public void onFinish() {
Log.d(TAG, "찾기 완료(Scan finish.)");
message = "";
mLoadingDialog.cancel();
mLoadingDialog.setCancelable(false);
mLoadingDialog.setOnCancelListener(null);
mDeviceListDialog.show();
}
});
}


private void connect(BluetoothDevice device) {
mLoadingDialog.setMessage("연결중(Connecting...)");
mLoadingDialog.setCancelable(false);
mLoadingDialog.show();

BluetoothClientService btSet = mClient;
btSet.connect(getApplicationContext(), device, mBTHandler);
}

private BluetoothClientService.BluetoothStreamingHandler mBTHandler = new BluetoothClientService.BluetoothStreamingHandler() {
ByteBuffer mmByteBuffer = ByteBuffer.allocate(1024);

@Override
public void onError(Exception e) {
mLoadingDialog.cancel();
addText("Message : Connection error - " + e.toString() + "\n");
//mMenu.getItem(0).setTitle(R.string.action_connect);
}

@Override
public void onDisconnected() {
//mMenu.getItem(0).setTitle(R.string.action_connect);
mLoadingDialog.cancel();
addText("Message : Disconnected.\n");
}

@Override
public void onData(byte[] buffer, int length) {
if (length == 0) return;
if (mmByteBuffer.position() + length >= mmByteBuffer.capacity()) {
ByteBuffer newBuffer = ByteBuffer.allocate(mmByteBuffer.capacity() * 2);
newBuffer.put(mmByteBuffer.array(), 0, mmByteBuffer.position());
mmByteBuffer = newBuffer;
}
mmByteBuffer.put(buffer, 0, length);
if (buffer[length - 1] == '\0') {
addText(mClient.getConnectedDevice().getName() + " : " +
new String(mmByteBuffer.array(), 0, mmByteBuffer.position()) + '\n');
mmByteBuffer.clear();
}
}

@Override
public void onConnected() {
addText("Message : Connected. " + mClient.getConnectedDevice().getName() + "\n");
mLoadingDialog.cancel();
//mMenu.getItem(0).setTitle(R.string.action_disconnect);
}
};

public void sendStringData(String data) {
data += '\0';
byte[] buffer = data.getBytes();
if (mBTHandler.write(buffer)) {
addText("Me : " + data + '\n');
}
}

protected void onDestroy() {
Log.d(TAG, onDestroy()");
super.onDestroy();
mClient.clear();
}

@Override
public boolean onCreateOptionsMenu(Menu menu){
super.onCreateOptionsMenu(menu);

MenuItem item1 = menu.add(0, 1, 0, "블루투스 on/OFF");
MenuItem item2 = menu.add(0, 2, 0, "연결(Connect)");
MenuItem item3 = menu.add(0, 3, 0, "내장치 보이기(Discovery)");
MenuItem item4 = menu.add(0, 4, 0, "아두이노 코드(Arduino Code)");

return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {

switch ( item.getItemId() ){

case 1:
enableBluetooth();
return true;

case 2:
pairDialog();
return true;

case 3:
discoveryDialog();
return true;

case 4:
showCodeDlg();
return true;

default:
return super.onOptionsItemSelected(item);
}

}

private void pairDialog() {
boolean connect = mClient.isConnection();

if (!connect && mClient.isEnabled()) {
getPairedDevices();
mDeviceListDialog.show();
} else {

Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
mBTHandler.close();
}
}

private void discoveryDialog(){

if( mClient.isEnabled()) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
}
else
{
Toast.makeText(getApplicationContext(), "블루투스 활성화 후 사용하세요.", Toast.LENGTH_SHORT).show();
}
}

private void showCodeDlg() {

TextView codeView = new TextView(this);
codeView.setText( Html.fromHtml(readCode()) );
codeView.setMovementMethod(new ScrollingMovementMethod());
codeView.setBackgroundColor(Color.parseColor("#202020"));

new AlertDialog.Builder(this, android.R.style.Theme_Holo_Light_DialogWhenLarge)
.setView(codeView)
.setPositiveButton("OK", new AlertDialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
}).show();
}

private String readCode() {

try {
InputStream is = getAssets().open("HC_06_Echo.txt");
int length = is.available();
byte[] buffer = new byte[length];
is.read(buffer);
is.close();
String code = new String(buffer);
buffer = null;
return code;
} catch (IOException e) {
e.printStackTrace();
}
return "";
}

private void checkBTEnabled(){

if ( mClient.isEnabled() ) {
initDeviceListDialog();
initWidget();
}
}

/*
* 안드로이드 버전 라인업(Line-up)에 따른 문제. (신규 추가)
*/
private void checkBTPermissions(){

// 안드로이드 SDK 버전이 LOLLIPOP보다 클 때,
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP){

int permissionCheck = this.checkSelfPermission("Manifest.permission.ACCESS_FINE_LOCATION");
permissionCheck += this.checkSelfPermission("Manifest.permission.ACCESS_COARSE_LOCATION");

if (permissionCheck != 0) {

this.requestPermissions( new String[] {
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION
}, 1001); //Any number
}

}else{
Log.d(TAG, "checkBTPermissions: No need to check permissions. SDK version < LOLLIPOP.");
Toast.makeText(getApplicationContext(), "SDK버전이 롤리팝 아래 버전으로 인해 권한을 상승할 수 없습니다..",
             Toast.LENGTH_SHORT).show();
finish();
}
}

}


 MainActivity.java

 

package com.localhost.kr.bluetoothchat;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Looper;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* Created by lab on 2017-11-21.
*/

public class BluetoothClientService {

public static final int REQUEST_ENABLE_BT = 1;

static final private String SERIAL_UUID = "00001101-0000-1000-8000-00805F9B34FB";

private BluetoothAdapter mBluetoothAdapter;
private OnScanListener mOnScanListener;
private BluetoothSocket mBluetoothSocket;

private UUID mUUID = UUID.fromString(SERIAL_UUID);
private AtomicBoolean mIsConnection = new AtomicBoolean(false);

private ExecutorService mReadExecutor;
private ExecutorService mWriteExecutor;

private BluetoothStreamingHandler mBluetoothStreamingHandler;
private Handler mMainHandler = new Handler(Looper.getMainLooper());

private BluetoothDevice mConnectedDevice = null;
private InputStream mInputStream;
private OutputStream mOutputStream;

static private BluetoothClientService sThis = null;
/**
* BluetoothSerialClient 의 싱글 인스턴스를 가져온다.
* @return BluetoothSerialClient 의 인스턴스. 만약 블루투스를 사용할 수 없는 기기라면 null.
*/
public static BluetoothClientService getInstance() {
if(sThis == null) {
sThis = new BluetoothClientService();
}
if(sThis.mBluetoothAdapter == null) {
sThis = null;
return null;
}
return sThis;
}

private BluetoothClientService() {
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mReadExecutor = Executors.newSingleThreadExecutor();
mWriteExecutor = Executors.newSingleThreadExecutor();

}

/**
* 연결을 닫고 자원을 해지한다.
* 앱 종료시 반드시 호출해 줘야 한다.
*/
public void clear() {
close();
mReadExecutor.shutdownNow();
mWriteExecutor.shutdownNow();
sThis = null;
}

/**
* 블루투스가 사용 가능한 상태인지 확인.
* @return false 라면 블루투스가 off 된 상태거나 사용할 수 없다.
*/
public boolean isEnabled() {
return mBluetoothAdapter.isEnabled();
}

/**
* 블루투스 디바이스와 시리얼로 연결한다.
* @param context
* @param device 블루투스 디바이스. {@link getPairedDevices} 또는 {@link scanDevices} 를 통하여
가져온 블루투스 디바이스 인스턴스.
* @param bluetoothStreamingHandler 블루투스 스트리밍 핸들러.
* @return 만약 블루투스를 사용할 수 없는 상태라면 false. {@link enableBluetooth} 를 통하여
블루투스를 사용 가능한 상태로 만들어줘야 한다.
*/
public boolean connect(final Context context,final BluetoothDevice device,
final BluetoothStreamingHandler bluetoothStreamingHandler) {

if(!isEnabled()) return false;
mConnectedDevice = device;
mBluetoothStreamingHandler = bluetoothStreamingHandler;
if(isConnection()) {
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mIsConnection.set(false);
mBluetoothSocket.close();

Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
connect(context, device, bluetoothStreamingHandler);
}
});
} else {
mIsConnection.set(true);
connectClient();
}
return true;
}


/**
* 과거에 페어링 되었던 블루투스 디바이스 목록을 가져온다.
* @return
*/
public Set<BluetoothDevice> getPairedDevices() {
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
return pairedDevices;
}

/**
* 주변의 새 블루투스 디바이스를 스캔한다.
* @param context
* @param OnScanListener 블루투스를 스캔 이벤트.
* @return
*/
public boolean scanDevices(Context context, onScanListener onScanListener) {

if(!mBluetoothAdapter.isEnabled()) return false;
if(mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
}
mOnScanListener = onScanListener;
IntentFilter filterFound = new IntentFilter(BluetoothDevice.ACTION_FOUND);
filterFound.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
filterFound.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
context.registerReceiver(mDiscoveryReceiver, filterFound);
mBluetoothAdapter.startDiscovery();
return true;
}

/**
* 스캔을 취소한다.
* @param context
*/
public void cancelScan(Context context) {
if(!mBluetoothAdapter.isEnabled() || !mBluetoothAdapter.isDiscovering()) return;
mBluetoothAdapter.cancelDiscovery();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
if(mOnScanListener != null) mOnScanListener.onFinish();
}

/**
* 블루투스 디바이스와 연결 되어있는지를 가져온다.
* @return true/false
*/
public boolean isConnection() {
return mIsConnection.get();
}

/**
* 연결된 블루투스 디바이스를 가져온다.
* @return 만약 연결된 블루투스 디바이스가 없다면 null.
*/
public BluetoothDevice getConnectedDevice() {
return mConnectedDevice;
}

private void connectClient() {
try {
mBluetoothSocket = mConnectedDevice.createRfcommSocketToServiceRecord(mUUID);
} catch (IOException e) {
close();
e.printStackTrace();
mBluetoothStreamingHandler.onError(e);
return;
}
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mBluetoothAdapter.cancelDiscovery();
mBluetoothSocket.connect();
manageConnectedSocket(mBluetoothSocket);
callConnectedHandlerEvent();
mReadExecutor.execute(mReadRunnable);
} catch (final IOException e) {
close();
e.printStackTrace();
mMainHandler.post(new Runnable() {
@Override
public void run() {
mBluetoothStreamingHandler.onError(e);
}
});
mIsConnection.set(false);
try {
mBluetoothSocket.close();
} catch (Exception ec) {
ec.printStackTrace();
}
}
}
});
}


private void manageConnectedSocket(BluetoothSocket socket) throws IOException {
mInputStream = socket.getInputStream();
mOutputStream = socket.getOutputStream();
}

private void callConnectedHandlerEvent() {
mMainHandler.post(new Runnable() {
@Override
public void run() {
mBluetoothStreamingHandler.onConnected();
}
});
}


private boolean write(final byte[] buffer) {
if(!mIsConnection.get()) return false;
mWriteExecutor.execute(new Runnable() {
@Override
public void run() {
try {
mOutputStream.write(buffer);
} catch (Exception e) {
close();
e.printStackTrace();
mBluetoothStreamingHandler.onError(e);
}
}
});
return true;
}


private boolean close() {
mConnectedDevice = null;
if(mIsConnection.get()) {
mIsConnection.set(false);
try {
mBluetoothSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
mMainHandler.post(mCloseRunnable);
return true;
}
return false;
}

private Runnable mCloseRunnable = new Runnable() {
@Override
public void run() {
if(mBluetoothStreamingHandler != null) {
mBluetoothStreamingHandler.onDisconnected();
}
}
};

private Runnable mReadRunnable = new Runnable() {
@Override
public void run() {
try {
final byte[] buffer = new byte[256];
final int readBytes = mInputStream.read(buffer);
mMainHandler.post(new Runnable() {
@Override
public void run() {
if(mBluetoothStreamingHandler != null) {
mBluetoothStreamingHandler.onData(buffer ,readBytes);
}
}
});
mReadExecutor.execute(mReadRunnable);
} catch (Exception e) {
close();
e.printStackTrace();
}
}
};


private BroadcastReceiver mDiscoveryReceiver = 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);
if(mOnScanListener != null) mOnScanListener.onFoundDevice(device);
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
if(mOnScanListener != null) mOnScanListener.onFinish();
try {
context.unregisterReceiver(mDiscoveryReceiver);
} catch(IllegalArgumentException e) {
e.printStackTrace();
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
if(mOnScanListener != null) mOnScanListener.onStart();
}
}
};

public static interface OnScanListener {
public void onStart();
public void onFoundDevice(BluetoothDevice bluetoothDevice);
public void onFinish();
}

public abstract static class BluetoothStreamingHandler {
public abstract void onError(Exception e);
public abstract void onConnected();
public abstract void onDisconnected();
public abstract void onData(byte[] buffer, int length);
public final boolean close() {
BluetoothClientService btSet = getInstance();
if(btSet != null)
return btSet.close();
return false;
}
public final boolean write(byte[] buffer) {
BluetoothClientService btSet = getInstance();
if(btSet != null)
return btSet.write(buffer);
return false;
}
}

}

 BluetoothClientService.java


[소스코드]

Rabbit-BluetoothChat.zip



8. 참고 자료

1. https://developer.android.com/guide/topics/connectivity/bluetooth.html, 안드로이드 블루투스 가이드

    - 안드로이드 공식 블루투스 가이드입니다. 최신 LE(Low Energy) 등에 대한 이야기도 소개하고 있습니다.

      공식 사이트입니다.

2. https://developer.android.com/samples/BluetoothChat/index.html, 블루투스 채트 - 셈플

3. https://stackoverflow.com/questions/36163751/android-marshmallow-6-0-1-bluetooth-scan-returning-no-results, Android Marshmallow 6.0.1 Bluetooth Scan Returning No Results 

4. http://www.hardcopyworld.com/ngine/android/index.php/archives/154, 블루투스 통신용 앱 구현하기 소스코드 – BluetoothChat

5. https://github.com/pablobuenaposada/arduino-HC-06/tree/master/Android, 블루투스 통신용 앱 구현하기 소스코드(Github)

        

* 메니페스트에 새 창 추가하는 방법

 


<!-- 여기서부터 추가 --> 
<activity android:name="bluetooth.DeviceListActivity" android:theme="@android:style/Theme.DeviceDefault.Dialog">

<!-- 여기까지 추가 -->



6. http://dev.re.kr/39, 안드로이드에서 시리얼 블루투스 디바이스 통신을 쉽게 하기 위한 클라이언트 클래스.

7. https://github.com/ice3x2/HC-06_Arduino_Echo/blob/master/src/kr/re/Dev/Bluetooth/BluetoothSerialClient.java

8. 소스코드 수정 / 변경.

       * 블루투스 활성화하지 않고 해당 소스코드를 실행하였을 때, 오류 발생.

    

 작업명

 해당 영역

 내용

 비고

 menu/main.xml

 

 // (mainActivity.java)

@Override
public boolean onCreateOptionsMenu(Menu menu){
    super.onCreateOptionsMenu(menu);
    MenuItem item1 = menu.add(0, 1, 0, "블루투스 on/OFF");

~~

public boolean onOptionsItemSelected(MenuItem item) {

   ....

      default:
          return super.onOptionsItemSelected(item);
     }
}

 제거

(수정자:

rabbit.white)

  src/BluetoothSerialClient.java39 Line private OnBluetoothEnabledListener mOnBluetoothUpListener;

 삭제

(수정자:

rabbit.white)

 src/BluetoothSerialClient.java

300~400 Line

400~403 Line

public static class BluetoothUpActivity extends Activity {
    private static int REQUEST_ENABLE_BT = 2; 

                       ~

public static interface onBluetoothEnabledListener {

     public void onBluetoothEnabled(boolean success); 
 }

 삭제

(수정자:

rabbit.white)

 src/BluetoothSerialClient.java

95~103 Line

 mOnBluetoothUpListener = onBluetoothEnabledListener;

 -> Intent intent = new Intent(context, BluetoothUpActivity.class);

 // 오류 발생함.

 삭제

(수정자:

rabbit.white)

 src/MainActivity.java

 

 private void enableBluetooth() {

        BluetoothClientService btSet = mClient;

    if ( !mClient.isEnabled() ){
         Intent intent = new Intent(BluetoothAdapter.ACTION_REQU  EST_ENABLE);
         startActivityForResult(intent, BluetoothClientService.REQUEST_ENABLE_BT);
       return;
    }

}

 변경/추가

(수정자:

rabbit.white)

src/MainActivity.java313~326 Line

 private void pairDialog() 변경

 // if ( mClient.isEnabled() && !connect )

 -> pairDialog() 호출

변경

(수정자:

rabbit.white)

 res/values/strings.xml

 6~8 Line

  <string name="action_connect">Connect</string>
  <string name="action_arduinocode">Show Arduino code</string>  <string name="action_disconnect">Disconnect</string> 
<string name="action_settings"></string>

제거

(수정자:

rabbit.white)

 src/MainActivity.java 

 initWidget();
 initDeviceListDialog();

 // 2개로 줄임.

변경

(수정자:

rabbit.white)

  

 // 이외에 다수 수정을 많이 하였음.

 


9. https://www.youtube.com/playlist?list=PLgCYzUzKIBE8KHMzpp6JITZ2JxTgWqDH2, How to Use Bluetooth in Android Studio, Mitch Tabian, Youtube, 2017. 4. 18.

- [구현 관련 영상]

   Bluetooth Tutorial - How to Pair, 2:37분(퍼미션에 관한 이야기 - 매니페스트)

   -> checkBTPermissions(), 롤리팝 LOLLIPOP SDK 버전 등에 대한 설명.

10. https://ko.wikipedia.org/wiki/%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4, 블루투스, 위키백과


Rabbit-BluetoothChat.zip
0.08MB
171124-present.pptx
0.1MB