權限
使用藍芽通訊, 需取得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){} } } }