權限
使用藍芽通訊, 需取得BLUETOOTH及BLUETOOTH_ADMIN權限, 此二個屬於一般授權. BLUETOOTH是讓 APK有權限連接裝置, 傳輸資料. BLUETOOTH_ADMIN則是讓APK有權限搜尋裝置及設定藍芽.
但有沒有搞錯, 為什麼第一行寫著需要GPS的危險權限呢. 依Google的文件說明, 在啟動藍芽搜尋附近的裝置時(startDiscovery), 為了給用戶提供更嚴格的數據保護, 需要GPS權限.
到底藍芽干GPS啥鳥事? 這是Google的神邏輯, 小弟也百思不得其”姐”, 請自行打電話去問Google.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
BlueToothAdapter
偵測裝置是否有藍芽設備, 可由系統取得BluetoothAdapter, 若無法取得Adapter, 表示裝置無藍芽
BluetoothAdapter btAdapter=BluetoothAdapter.getDefaultAdapter();
if(btAdapter==null){
AlertDialog.Builder dialog=new AlertDialog.Builder(this);
dialog
.setTitle("Bluetooth Test")
.setMessage(("裝置沒有藍芽設備"))
.show();
}
使用BluetoothAdapter的isEnabled() 可以判斷藍芽是否有被開啟. 如果沒有的話, 可以使用BluetoothAdapter.ACTION_REQUEST_ENABLE跳到開啟視窗, 要求使用者開啟.
另外, 也可以使用btAdapter.enable();直接開啟藍芽, 不需經過使用者同意
if(!btAdapter.isEnabled()){
Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, 100);
/*App自已直接開啟藍芽
btEnable();
processPermission();
*/
}
開啟藍芽回應
上面startActivityForResult()方法會跳到開啟藍芽的視窗, 待使用者按下確定或取消後, 會回到本程式並執行onActivityResult() callback, 所以必需撰寫onActivityResult()接收回應, 請按下Ctrl+O實作此方法. 相關代碼如下
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==100){
if(resultCode==Activity.RESULT_CANCELED){
new AlertDialog.Builder(this)
.setMessage("藍芽未開啟, 即將結束")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
else if(resultCode==Activity.RESULT_OK){
processPermission();
}
}
}
取得已配對裝置
第一次與遠端裝置建立連線時, 會對遠端發出一個配對請求. 當配對完成, 遠端裝置的基本資訊(如名稱, class, MAC address) 就會被儲存起來. 以後就可以直接連線, 不需重新配對.
要取得本機端以前曾經跟誰配對過, 可以使用 BluetoothAdapter的getBondedDevices() , 此方法會傳回 Set<BluetoothDevice> 資料型態, 然後就可由BluetoothDevice取得遠端裝置的名稱, mac等資訊.
private void init(){
Set<BluetoothDevice> pairedDevice=btAdapter.getBondedDevices();
String s="";
if(pairedDevice.size()>0){
for(BluetoothDevice device: pairedDevice){
s+=String.format("%s : %s\n", device.getAddress(), device.getName());
}
txt.setText(s);
}
搜尋附近裝置
接下來就可以搜尋附近的裝置了, 使用btAdapter.startDiscovery()開始搜尋. 如果系統有找到可用的藍芽裝置, 就會廣播 BluetoothDevice.ACTION_FOUND的訊號. 因此為了要能接收到此訊號, 就必需自訂一個receiver物件 .
請注意, 如上所說的, 自Android 6.0開始, startDiscovery()必需要開啟GPS權限, 否則是找不到裝置的.
BroadcastReceiver btReceiver=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)){
BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
strUnPaired+=String.format("%s : %s\n", device.getAddress(), device.getName());
txtUnPaired.setText(strUnPaired);
}
}
};
然後在MainActivity註冊並開始搜尋. 相關代碼如下
IntentFilter filter=new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); registerReceiver(btReceiver, filter); btAdapter.startDiscovery();
將上述的步驟整合起來如下代碼
public class MainActivity extends AppCompatActivity {
TextView txtPaired, txtUnPaired;
BluetoothAdapter btAdapter;
String strPaired="", strUnPaired="";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
txtPaired=(TextView)findViewById(R.id.txtPaired);
txtUnPaired=(TextView)findViewById(R.id.txtUnPaired);
txtPaired.setMovementMethod(new ScrollingMovementMethod());
txtUnPaired.setMovementMethod(new ScrollingMovementMethod());
btAdapter=BluetoothAdapter.getDefaultAdapter();
if(btAdapter==null){
new AlertDialog.Builder(this)
.setTitle("Bluetooth Test")
.setMessage(("裝置沒有藍芽設備, 即將結束"))
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
if(!btAdapter.isEnabled()){
Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, 100);
}
else{
processPermission();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==100){
if(resultCode==Activity.RESULT_CANCELED){
new AlertDialog.Builder(this)
.setMessage("藍芽未開啟, 即將結束")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
else if(resultCode==Activity.RESULT_OK){
processPermission();
}
}
}
private void init(){
Set<BluetoothDevice> pairedDevice=btAdapter.getBondedDevices();
if(pairedDevice.size()>0){
for(BluetoothDevice device: pairedDevice){
strPaired+=String.format("%s : %s\n", device.getAddress(), device.getName());
}
txtPaired.setText(strPaired);
}
IntentFilter filter=new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);
registerReceiver(btReceiver, filter);
btAdapter.startDiscovery();
}
BroadcastReceiver btReceiver=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)){
BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
strUnPaired+=String.format("%s : %s\n", device.getAddress(), device.getName());
txtUnPaired.setText(strUnPaired);
}
}
};
@Override
protected void onDestroy() {
btAdapter.cancelDiscovery();
unregisterReceiver(btReceiver);
super.onDestroy();
}
private void processPermission(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){
int hasPermission=checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
if(hasPermission!= PackageManager.PERMISSION_GRANTED){
requestPermissions(
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},200);
}
else{
init();
}
}
else init();
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if(requestCode==200){
if (grantResults[0]==PackageManager.PERMISSION_GRANTED)init();
else Toast.makeText(this, "No permission", Toast.LENGTH_LONG).show();
}
else super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
配對請求
未配對過的裝置, 可以使用Method createBondMethod進行配對並連線.
開始配對時, 會產生ACTION_BOND_STATE_CHENGE廣播. 所以IntentFilter需加如下條件
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
然後在Receiver裏加入如下狀態顯示
在下面的EXTRA_BOND_STATE及EXTRA_PREVOIUS_BOND有三種狀態
BOND_NONE : 10
BOND_BONDING : 11
BOND_BONDED : 12
else if(intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
int cur_bond_state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
int previous_bond_state = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE);
Log.d("Thomas", "### cur_bond_state ##" + cur_bond_state + " ~~ previous_bond_state" + previous_bond_state);
}
底下是開始配對請求的代碼, 其中的”DEB515″ 是本人的藍芽耳機, 請自行更改
public void btnBondClick(View view){
Method createBondMethod = null;
try {
createBondMethod = BluetoothDevice.class.getMethod("createBond");
for(BluetoothDevice d:unBondDevice) {
if(d.getName()!=null && d.getName().equals("DEB515")) {
Log.d("Thomas","開始配對");
createBondMethod.invoke(d);
break;
}
}
}
catch (NoSuchMethodException e) {}
catch (IllegalAccessException e) {}
catch (InvocationTargetException e) {}
}
連線
一般的藍芽耳機, 遙桿等, 都是第一次配對成功後, 以後打開即會自動連線. 但如果要跟其他手機連線, 則必需手動按連線. 所以相關代碼如下
下面代碼中, SPP_UUID的意思, 是指藍芽使用了SPP通訊協定, 而UUID(Universally Unique Identifier)是藍芽的通訊埠, 如同電腦IP上的 port.
UUID的格式分成5段, 中間3段為4個字, 第1段8個字, 最後一段是12個字. 所以UUID格式為
8-4-4-4-12 字串‧
UUID 也有一些預設值. 如下
模擬成Serial Port : 00001101-0000-1000-8000-00805F9B34FB
資訊同步服務 : 00001104-0000-1000-8000-00805F9B34FB
檔案傳輸服務 : 00001106-0000-1000-8000-00805F9B34FB
private void connect(BluetoothDevice btDev) {
final String SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB";
UUID uuid = UUID.fromString(SPP_UUID);
try {
BluetoothSocket btSocket = btDev.createRfcommSocketToServiceRecord(uuid);
Log.d("Thomas", "Starting connection...");
btSocket.connect();
} catch (IOException e) {
e.printStackTrace();
}
}
傳送接收
下面代碼, 可以在二支手機裏, 使用藍芽互傳文字訊息.

package com.asuscomm.mahaljsp.bluetoothtest;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
TextView txtMsg;
EditText editMsg;
ListView listViewPaired, listViewUnPaire;
BluetoothAdapter btAdapter;
Set<BluetoothDevice> deviceSet=new HashSet<>();
List<BluetoothDevice>bondDeviceList=new ArrayList<>();
List<BluetoothDevice>unBondDeviceList=new ArrayList<>();
List<HashMap<String, String>> pairedList=new ArrayList<>(), unPaireList=new ArrayList<>();
SimpleAdapter pairedAdapter, unPaireAdapter;
private OutputStream outputStream;
private InputStream inputStream;
//final String SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB";
final String SPP_UUID = "abcd0000-1234-1234-1234-abcd12345678";
UUID uuid = UUID.fromString(SPP_UUID);
BluetoothSocket clientSocket;
private BluetoothServerSocket serverSocket;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listViewPaired=(ListView)findViewById(R.id.listViewPaired);
listViewUnPaire=(ListView)findViewById(R.id.listViewUnPaire);
txtMsg=(TextView)findViewById(R.id.txtMsg);
txtMsg.setMovementMethod(new ScrollingMovementMethod());
editMsg=(EditText)findViewById(R.id.editMsg);
btAdapter=BluetoothAdapter.getDefaultAdapter();
if(btAdapter==null){
new AlertDialog.Builder(this)
.setTitle("Bluetooth Test")
.setMessage(("裝置沒有藍芽設備, 即將結束"))
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
if(!btAdapter.isEnabled()){
//Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
//startActivityForResult(intent, 100);
btAdapter.enable();
processPermission();
}
else{
processPermission();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode==100){
if(resultCode==Activity.RESULT_CANCELED){
new AlertDialog.Builder(this)
.setMessage("藍芽未開啟, 即將結束")
.setPositiveButton("確定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
})
.show();
}
else if(resultCode==Activity.RESULT_OK){
processPermission();
}
}
}
private void init(){
//設定已配對 ListView 的 Adapter及Click事件
pairedAdapter=new SimpleAdapter(
this,
pairedList,
R.layout.listview_layout,
new String[]{"Name","Mac"},
new int[]{R.id.txtName, R.id.txtMac}
);
listViewPaired.setAdapter(pairedAdapter);
listViewPaired.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "connecting......", Toast.LENGTH_LONG).show();
String mac=pairedList.get(position).get("Mac");
BluetoothDevice device=btAdapter.getRemoteDevice(mac);
try {
clientSocket = device.createRfcommSocketToServiceRecord(uuid);
clientSocket.connect();
outputStream=clientSocket.getOutputStream();
inputStream=clientSocket.getInputStream();
serverSocket.close();//當裝置為client時, 要中斷下面執行緒裏的 serverSocket.accept() blocking call
} catch (IOException e) {
e.printStackTrace();
}
}
});
//設定搜尋 ListView 的 Adapter及Click事件
unPaireAdapter=new SimpleAdapter(
this,
unPaireList,
R.layout.listview_layout,
new String[]{"Name","Mac"},
new int[]{R.id.txtName, R.id.txtMac}
);
listViewUnPaire.setAdapter(unPaireAdapter);
listViewUnPaire.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String s=unPaireList.get(position).get("Mac");
Log.d("Thomas", s);
bondDevice(unPaireList.get(position).get("Mac"));
}
});
deviceSet=btAdapter.getBondedDevices();
if(deviceSet.size()>0){
for(BluetoothDevice device: deviceSet){
HashMap<String, String>hashMap=new HashMap<>();
hashMap.put("Name", device.getName());
hashMap.put("Mac", device.getAddress());
pairedList.add(hashMap);
pairedAdapter.notifyDataSetChanged();
bondDeviceList.add(device);
}
}
IntentFilter filter=new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(btReceiver, filter);
btAdapter.startDiscovery();
new ReceivedThread().start();
}
BroadcastReceiver btReceiver=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)){
BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
unBondDeviceList.add(device);
HashMap<String, String>hashMap=new HashMap<>();
hashMap.put("Name", device.getName());
hashMap.put("Mac", device.getAddress());
unPaireList.add(hashMap);
unPaireAdapter.notifyDataSetChanged();
}
else if(intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
int cur_bond_state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
int previous_bond_state = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE);
Log.d("Thomas", "### cur_bond_state ##" + cur_bond_state + " ~~ previous_bond_state" + previous_bond_state);
if(cur_bond_state==BluetoothDevice.BOND_BONDED){
BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
HashMap<String, String>hashMap=new HashMap<>();
hashMap.put("Name", device.getName());
hashMap.put("Mac", device.getAddress());
pairedList.add(hashMap);
pairedAdapter.notifyDataSetChanged();
bondDeviceList.add(device);
unBondDeviceList.remove(device);
for (HashMap<String, String >h : unPaireList){
if(h.get("Mac").equals(device.getAddress())){
unPaireList.remove(h);
unPaireAdapter.notifyDataSetChanged();
break;
}
}
}
}
}
};
@Override
protected void onDestroy() {
btAdapter.cancelDiscovery();
unregisterReceiver(btReceiver);
super.onDestroy();
}
private void processPermission(){
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){
int hasPermission=checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION);
if(hasPermission!= PackageManager.PERMISSION_GRANTED){
requestPermissions(
new String[]{Manifest.permission.ACCESS_FINE_LOCATION},200);
}
else{
init();
}
}
else init();
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if(requestCode==200){
if (grantResults[0]==PackageManager.PERMISSION_GRANTED)init();
else Toast.makeText(this, "No permission", Toast.LENGTH_LONG).show();
}
else super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void bondDevice(String mac){
Method createBondMethod = null;
try {
createBondMethod = BluetoothDevice.class.getMethod("createBond");
for(BluetoothDevice d:unBondDeviceList) {
if(d.getAddress()!=null && d.getAddress().equals(mac)) {
Log.d("Thomas","開始配對");
createBondMethod.invoke(d);
break;
}
}
}
catch (NoSuchMethodException e) {Log.d("Thomas", e.getMessage());}
catch (IllegalAccessException e){Log.d("Thomas", e.getMessage());}
catch (InvocationTargetException e) {Log.d("Thomas", e.getMessage());}
}
public void btnSendClick(View view){
if(outputStream!=null){
try {
outputStream.write(editMsg.getText().toString().getBytes("utf-8"));
editMsg.setText("");
} catch (IOException e) {
Log.d("Thomas", "Send error : "+e.getMessage());
}
}
else{
Log.d("Thomas","os is null");
}
}
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
String s=txtMsg.getText().toString();
switch(msg.what){
case 100:
txtMsg.setText(s+"\n"+(String)(msg.obj));
break;
case 200:
Toast.makeText(MainActivity.this, "Connected", Toast.LENGTH_SHORT).show();
break;
}
}
};
private class ReceivedThread extends Thread {
public ReceivedThread() {
try {
serverSocket = btAdapter.listenUsingRfcommWithServiceRecord("Bluetooth_Socket", uuid);
} catch (Exception e) {}
}
public void run() {
try {
Log.d("Thomas", "blocked");
BluetoothSocket socket = serverSocket.accept();//blocking call
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
}
catch(Exception e){
Log.d("Thomas", "server socket close");
Message msg=new Message();
msg.what=200;
handler.sendMessage(msg);
}
try {
while (true) {
byte[] buffer = new byte[1024];
int count = inputStream.read(buffer);
Message msg = new Message();
msg.what=100;
msg.obj = new String(buffer, 0, count, "utf-8");
handler.sendMessage(msg);
Log.d("Thomas", "received");
}
}
catch(IOException e){}
}
}
}
