Android經(jīng)典藍(lán)牙開發(fā)

前言

此外本文只涉及經(jīng)典藍(lán)牙(Classic Bluetooth)的點(diǎn)對(duì)點(diǎn)通信開發(fā)豪娜,并不涉及低功耗藍(lán)牙(BLE)的開發(fā)房蝉。

開發(fā)流程

  • 設(shè)置藍(lán)牙
  • 搜索附近的藍(lán)牙設(shè)備
  • 配對(duì)連接
  • 通信

設(shè)置藍(lán)牙

1.獲取 BluetoothAdapter:

 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

2.開啟藍(lán)牙

  • 處理6.0以下版本的藍(lán)牙權(quán)限

1.在AndroidManifest中添加權(quán)限:

<!-- 應(yīng)用使用藍(lán)牙的權(quán)限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 掃描藍(lán)牙設(shè)備或者操作藍(lán)牙設(shè)置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

2.開啟藍(lán)牙功能:

常量REQUEST_ENABLE_BT是本地定義的整型(需要大于0)矾端,當(dāng)系統(tǒng)通過onActivityResult() 返回至你的應(yīng)用程序時(shí),將作為requestCode的參數(shù)。
如果成功開啟了藍(lán)牙果善,你的Activity將收到RESULT_OK作為resultCode。如果藍(lán)牙不能成功開啟(例如用戶選擇“取消”)美侦,則resultCode為RESULT_CANCELED

//1产舞、獲取BluetoothAdapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//2、判斷是否支持藍(lán)牙菠剩,并彈窗要求打開藍(lán)牙
if(mBluetoothAdapter == null ||!mBluetoothAdapter.isEnabled()){
     Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
     startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);
}

3.對(duì)返回值進(jìn)行處理

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     super.onActivityResult(requestCode, resultCode, data);
     if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_OK){
           //已啟用易猫,進(jìn)行下一步初始化工作
     }else if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED){
           //未啟用,退出應(yīng)用
           Toast.makeText(MainActivity.this,"請(qǐng)啟用藍(lán)牙",Toast.LENGTH_SHORT).show();  
           finish();  
     }
}
  • 處理6.0版本以上的藍(lán)牙權(quán)限
    1.在AndroidManifest中添加一個(gè)模糊定位的權(quán)限:
<!--模糊定位權(quán)限具壮,僅作用于6.0+-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

2.校驗(yàn)藍(lán)牙權(quán)限:

if (Build.VERSION.SDK_INT >= 23) {
      //校驗(yàn)是否已具有模糊定位權(quán)限
      if (ContextCompat.checkSelfPermission(context,
      Manifest.permission.ACCESS_COARSE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(context,
                  new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                  REQUEST_ENABLE_BT
                  );
      } else {
            //具有權(quán)限
      }
} else {
      //系統(tǒng)不高于6.0執(zhí)行下一步初始化
}

3.對(duì)返回值進(jìn)行處理准颓,類似于startActivityForResult方法:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
      super.onRequestPermissionsResult(requestCode, permissions, grantResults);
      if (requestCode == REQUEST_ENABLE_BT) {
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //同意權(quán)限
            } else {
            // 權(quán)限拒絕
            }
      }
}

搜索附近的藍(lán)牙設(shè)備

1.查詢已配對(duì)的設(shè)備并加入列表

//將配過對(duì)的設(shè)備加入list
Set<BluetoothDevice> paireDevices = mBluetoothAdapter.getBondedDevices();
if(paireDevices.size()>0){
      for(BluetoothDevice device: paireDevices){
            adapter.addData(device); //adapter為列表的適配器
      }
}

2.發(fā)現(xiàn)設(shè)備
調(diào)用異步方法startDiscovery() 開始搜索藍(lán)牙設(shè)備。

該進(jìn)程為異步進(jìn)程棺妓,并且該方法會(huì)立即返回一個(gè)布爾值攘已,指示是否已成功啟動(dòng)發(fā)現(xiàn)操作。 發(fā)現(xiàn)進(jìn)程通常包含約 12 秒鐘的查詢掃描涧郊,之后對(duì)每臺(tái)發(fā)現(xiàn)的設(shè)備進(jìn)行頁(yè)面掃描贯被,以檢索其藍(lán)牙名稱。

當(dāng)這個(gè)方法發(fā)現(xiàn)藍(lán)牙設(shè)備時(shí)妆艘,將會(huì)廣播ACTION_FOUNDIntent 彤灶,搜索到的設(shè)備信息EXTRA_DEVICE包含在此Intent中,因此注冊(cè)一個(gè)BroadcastReceiver 來處理廣播批旺。

//新建一個(gè)IntentFilter
private IntentFilter getIntentFilter(){
      IntentFilter intentFilter = new IntentFilter();    
      intentFilter.addAction(BluetoothDevice.ACTION_FOUND);         
      intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); 
      intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
      return intentFilter;
}

//新建BroadcastReceiver
private final BroadcastReceiver receiver = 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(!list.contains(device)){//去重
                        adapter.addData(device);
                  }      
            }else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){                              
                  Toast.makeText(context,"掃描完畢",Toast.LENGTH_SHORT).show();
        }
    }
};

//在onCreate 中注冊(cè)
@Override
protected void onCreate(Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //注冊(cè)
        registerReceiver(receiver,getIntentFilter());
}

不要忘記在onDestroy()中進(jìn)行反注銷

unregisterReceiver(receiver);

執(zhí)行設(shè)備搜索的操作是一項(xiàng)很繁重的任務(wù)幌陕,會(huì)消耗大量的資源。一旦你找到了一個(gè)設(shè)備并要進(jìn)行連接汽煮,請(qǐng)務(wù)必確認(rèn)是否停止搜索設(shè)備的操作搏熄。如果已經(jīng)進(jìn)行了連接聪全,那么搜索操作將會(huì)顯著地降低連接的速率敢订,因此你應(yīng)當(dāng)在連接時(shí)停止搜索」┎螅可通過cancelDiscovery()方法停止搜索鞋囊。

配對(duì)連接

要在兩臺(tái)設(shè)備之間創(chuàng)建連接止后,其中一臺(tái)設(shè)備要作為服務(wù)器端,保持開放的BluetoothServerSocket并在線程中調(diào)用 accept()開始偵聽連接請(qǐng)求溜腐,
而另一臺(tái)設(shè)備必須利用掃描得到的服務(wù)端MAC發(fā)起連接請(qǐng)求译株。

:如果兩臺(tái)設(shè)備之前尚未配對(duì),則在連接過程中挺益,Android 框架會(huì)自動(dòng)向用戶顯示配對(duì)請(qǐng)求通知或?qū)υ捒?/p>

  • 服務(wù)器端:
  1. 通過調(diào)用listenUsingRfcommWithServiceRecord(String, UUID)獲取BluetoothServerSocket
  2. 通過在run()中調(diào)用accept()歉糜,開始監(jiān)聽連接請(qǐng)求。
    由于accept()為阻塞調(diào)用望众,所以需要一個(gè)專門的線程進(jìn)行連接的操作匪补。
//用于接收連接請(qǐng)求,并啟動(dòng)ConnectedThread
private class AcceptThread extends Thread {
      private final BluetoothServerSocket mmServerSocket;
      public AcceptThread() {
            BluetoothServerSocket tmp = null;
            try {
                  tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord("YourAPPName",                        MY_UUID);
            } catch (IOException e) {
                  e.printStackTrace();
            }
            mmServerSocket = tmp;
    }    
public void run() {
        Log.d(TAG, "BEGIN mAcceptThread" + this);
        BluetoothSocket socket = null;
        // 在沒有連接上的時(shí)候accept
        while (mState!=3) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                Log.e(TAG, "accept() failed", e);
                break;
            }
            if (socket != null) {
                synchronized (MainActivity.this) {
                    switch (mState) {
                        case STATE_LISTEN:
                        case STATE_CONNECTING:
                            // 準(zhǔn)備通信
                            connected(socket);
                            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");
    }
    public void cancel() {
        Log.d(TAG, "Socket cancel " + this);
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "Socket close() of server failed", e);
        }
    }
}

mState為標(biāo)記當(dāng)前狀態(tài)的變量伞辛,規(guī)定為

STATE_NONE = 0;       // 初始狀態(tài)
STATE_LISTEN = 1;     // 等待連接
STATE_CONNECTING = 2; // 正在連接
STATE_CONNECTED = 3;  // 已經(jīng)連接上設(shè)備
  • 客戶端:
  1. 利用掃描到的服務(wù)器端的MAC地址得到遠(yuǎn)程設(shè)備
    BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(macAddress);
  2. 該遠(yuǎn)程設(shè)備調(diào)用方法createRfcommSocketToServiceRecord(UUID)建立安全連接。

注:UUID定義為00001101-0000-1000-8000-00805F9B34FB叉袍,為手機(jī)藍(lán)牙串口的統(tǒng)一UUID始锚。

  1. run()中通過調(diào)用conenct建立連接
    由于connect()為阻塞調(diào)用,因此該連接過程應(yīng)始終在主 Activity 線程以外的線程中執(zhí)行喳逛。
//用于藍(lán)牙連接
private class ConnectThread extends Thread {
        private final BluetoothSocket mmSocket;
        private final BluetoothDevice mmDevice;

        public ConnectThread(BluetoothDevice device) {
                    mmDevice = device;
                    BluetoothSocket tmp = null;
                    try {
                        //嘗試建立安全的連接
                        tmp = mmDevice.createRfcommSocketToServiceRecord(MY_UUID);
                        //嘗試建立不安全的連接
                        //tmp = mmDevice.createInsecureRfcommSocketToServiceRecord(MY_UUID);
                    } catch (IOException e) {
                        Log.i(TAG,"獲取 BluetoothSocket失敗");
                        e.printStackTrace();
                    }
                    mmSocket = tmp;
     }
    @Override
    public void run() {
        if(mBluetoothAdapter.isDiscovering()){        
            mBluetoothAdapter.cancelDiscovery();
        }
        try {
            mmSocket.connect();
        } catch (IOException e) {
            Log.i(TAG,"socket連接失敗");
            //利用Handler傳遞消息
            Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
            Bundle bundle = new Bundle();
            bundle.putString(Constants.TOAST,"Socket連接失敗");
            msg.setData(bundle);
            mHandler.sendMessage(msg);
            return;
        } 
        synchronized (MainActivity.this){
            mConnectThread = null;
        }
        //調(diào)用外部類方法,啟動(dòng)用于通信線程connectedThread
        connected(mmSocket);
    }

    public void cancel(){
        try {
            mmSocket.close();
            setState(false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注:connected(mmSocket)是應(yīng)用中的虛構(gòu)方法棵里,它將啟動(dòng)用于傳輸數(shù)據(jù)的線程润文。

//連接完成后啟動(dòng)ConnectedThread
public synchronized void connected(BluetoothSocket socket){
    if (mConnectThread != null) {
        mConnectThread.cancel();
        mConnectThread = null;
    }
    if (mConnectedThread != null) {
        mConnectedThread.cancel();
        mConnectedThread = null;
    }
    setState(STATE_CONNECTED);
    mConnectedThread = new ConnectedThread(socket);
    mConnectedThread.start();
}

通信

在成功連接兩臺(tái)設(shè)備后,每臺(tái)設(shè)備都會(huì)有一個(gè)已連接的 BluetoothSocket殿怜。利用 BluetoothSocket傳輸任意數(shù)據(jù)的一般過程非常簡(jiǎn)單:

  1. 獲取 InputStreamOutputStream典蝌,二者分別通過套接字
    以及 getInputStream()getOutputStream()來處理數(shù)據(jù)傳輸。
  2. 使用 read(byte[])write(byte[])讀取數(shù)據(jù)并寫入到流式傳輸头谜。

因?yàn)閞ead(byte[]) 和 write(byte[])方法都是阻塞調(diào)用的骏掀,所以需要一個(gè)專門的線程進(jìn)行讀寫的操作。

//藍(lán)牙連接完成后進(jìn)行輸入輸出
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;
            try {
                  tmpIn = socket.getInputStream();
                  tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                  Log.e(TAG, "temp sockets not created", e);
            }
            mmInStream = tmpIn;
            mmOutStream = tmpOut;
      }    
      public void run() {
            Log.i(TAG, "BEGIN mConnectedThread");
            //當(dāng)連接狀態(tài)為連接時(shí)柱告,循環(huán)讀取
            while(mState == STATE_CONNECTED){
                  try {
                        // 從InputStream中讀取
                        Scanner in = new Scanner(mmInStream,"UTF-8");
                        String str = in.nextLine();
                        Log.i(TAG,"read: "+str);
                        //利用handle傳遞數(shù)據(jù)截驮,此時(shí)為Toast模式
                        Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
                        Bundle bundle = new Bundle();
                        bundle.putString(Constants.TOAST,str);
                        msg.setData(bundle);
                        mHandler.sendMessage(msg);
                  } catch (Exception e) {
                        Log.e(TAG, "disconnected", e);
                  }
           }
      }
      public void write(byte[] buffer) {
            try {
                  mmOutStream.write(buffer);
            } catch (IOException e) {
                  e.printStackTrace();
                  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);
            }
      }
}
  • 調(diào)用mConnectedThread.write(byte[] buffer)進(jìn)行輸出。
  • 輸入流則在run()中被循環(huán)讀取际度,這里采用了Handler處理數(shù)據(jù)傳遞葵袭。
    Constants的常量代表了對(duì)Message不同的處理方式,在ConnectedThreadrun()中乖菱,使用不同的 Constants值坡锡,調(diào)整輸入流的處理方式。

例子中使用了
mHandler.obtainMessage(Constants.MESSAGE_TOAST);
代表把得到的數(shù)據(jù)以MESSAGE_TOAST的方式處理窒所。

//利用Handler傳遞數(shù)據(jù)
private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
          switch(msg.what){
                case: Constants.SomeConfig:
                // do something
                break;
          }
    }
};

Demo的GitHub鏈接:https://github.com/YangLuYang/android-Demo-ClassicBluetooth

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鹉勒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子吵取,更是在濱河造成了極大的恐慌禽额,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件海渊,死亡現(xiàn)場(chǎng)離奇詭異绵疲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)臣疑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門盔憨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讯沈,你說我怎么就攤上這事郁岩⌒霰迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵问慎,是天一觀的道長(zhǎng)萍摊。 經(jīng)常有香客問我,道長(zhǎng)如叼,這世上最難降的妖魔是什么冰木? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮笼恰,結(jié)果婚禮上踊沸,老公的妹妹穿的比我還像新娘。我一直安慰自己社证,他們只是感情好逼龟,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著追葡,像睡著了一般腺律。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宜肉,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天匀钧,我揣著相機(jī)與錄音,去河邊找鬼崖飘。 笑死榴捡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的朱浴。 我是一名探鬼主播吊圾,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼翰蠢!你這毒婦竟也來了项乒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤梁沧,失蹤者是張志新(化名)和其女友劉穎檀何,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體廷支,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡频鉴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恋拍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垛孔。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖施敢,靈堂內(nèi)的尸體忽然破棺而出周荐,到底是詐尸還是另有隱情狭莱,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布概作,位于F島的核電站腋妙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏讯榕。R本人自食惡果不足惜骤素,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望愚屁。 院中可真熱鬧谆甜,春花似錦、人聲如沸集绰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)栽燕。三九已至,卻和暖如春改淑,著一層夾襖步出監(jiān)牢的瞬間碍岔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工朵夏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蔼啦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓仰猖,卻偏偏與公主長(zhǎng)得像捏肢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子饥侵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 公司的項(xiàng)目最近需要用到藍(lán)牙開發(fā)的相關(guān)內(nèi)容鸵赫,因此特地查閱了Google官方文檔的內(nèi)容并進(jìn)行二次整理,希望能對(duì)需要學(xué)習(xí)...
    Chuckiefan閱讀 32,418評(píng)論 44 123
  • 1.簡(jiǎn)介 通過藍(lán)牙API躏升,可以實(shí)現(xiàn)以下內(nèi)容: 掃描其他藍(lán)牙設(shè)備 查詢配對(duì)藍(lán)牙設(shè)備的本地藍(lán)牙適配器 創(chuàng)建RFCOMM...
    justCode_閱讀 4,808評(píng)論 0 3
  • 藍(lán)牙開發(fā)相關(guān) 使用Android Bluetooth APIs將設(shè)備通過藍(lán)牙連接并通信辩棒,設(shè)置藍(lán)牙,查找藍(lán)牙設(shè)備膨疏,配...
    CoderMiner閱讀 13,062評(píng)論 3 31
  • 今天跟媳婦吵架了,心情不好,所以想看個(gè)電影,到網(wǎng)上搜搜看,真的感覺沒什么好看的電影,也許是因?yàn)樾那椴缓玫木壒?..
    孩子的奶爸閱讀 185評(píng)論 0 0
  • 今天給大家聊一個(gè)輕松的事: 其實(shí)社群營(yíng)銷在過去很長(zhǎng)時(shí)間都一直存在,比如民國(guó)時(shí)間的奇女子林徽因,她就在做社群一睁。 林徽...
    威樂河灣閱讀 332評(píng)論 0 0