Bluetooth

      在〈Bluetooth〉中尚無留言

權限

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

傳送接收

下面代碼, 可以在二支手機裏, 使用藍芽互傳文字訊息.

bluetooth

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

完整代碼下載

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *