Android经典蓝牙开发

前言

此外本文只涉及经典蓝牙(Classic Bluetooth)的点对点通信开发,并不涉及低功耗蓝牙(BLE)的开发。

开发流程

  • 设置蓝牙
  • 搜索附近的蓝牙设备
  • 配对连接
  • 通信

设置蓝牙

1.获取 BluetoothAdapter:

 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

2.开启蓝牙

  • 处理6.0以下版本的蓝牙权限

1.在AndroidManifest中添加权限:

<!-- 应用使用蓝牙的权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 扫描蓝牙设备或者操作蓝牙设置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

2.开启蓝牙功能:

常量REQUEST_ENABLE_BT是本地定义的整型(需要大于0),当系统通过onActivityResult() 返回至你的应用程序时,将作为requestCode的参数。
如果成功开启了蓝牙,你的Activity将收到RESULT_OK作为resultCode。如果蓝牙不能成功开启(例如用户选择“取消”),则resultCode为RESULT_CANCELED

//1、获取BluetoothAdapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//2、判断是否支持蓝牙,并弹窗要求打开蓝牙
if(mBluetoothAdapter == null ||!mBluetoothAdapter.isEnabled()){
     Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
     startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);
}

3.对返回值进行处理

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     super.onActivityResult(requestCode, resultCode, data);
     if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_OK){
           //已启用,进行下一步初始化工作
     }else if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED){
           //未启用,退出应用
           Toast.makeText(MainActivity.this,"请启用蓝牙",Toast.LENGTH_SHORT).show();  
           finish();  
     }
}
  • 处理6.0版本以上的蓝牙权限
    1.在AndroidManifest中添加一个模糊定位的权限:
<!--模糊定位权限,仅作用于6.0+-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

2.校验蓝牙权限:

if (Build.VERSION.SDK_INT >= 23) {
      //校验是否已具有模糊定位权限
      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 {
            //具有权限
      }
} else {
      //系统不高于6.0执行下一步初始化
}

3.对返回值进行处理,类似于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) {
            //同意权限
            } else {
            // 权限拒绝
            }
      }
}

搜索附近的蓝牙设备

1.查询已配对的设备并加入列表

//将配过对的设备加入list
Set<BluetoothDevice> paireDevices = mBluetoothAdapter.getBondedDevices();
if(paireDevices.size()>0){
      for(BluetoothDevice device: paireDevices){
            adapter.addData(device); //adapter为列表的适配器
      }
}

2.发现设备
调用异步方法startDiscovery() 开始搜索蓝牙设备。

该进程为异步进程,并且该方法会立即返回一个布尔值,指示是否已成功启动发现操作。 发现进程通常包含约 12 秒钟的查询扫描,之后对每台发现的设备进行页面扫描,以检索其蓝牙名称。

当这个方法发现蓝牙设备时,将会广播ACTION_FOUNDIntent ,搜索到的设备信息EXTRA_DEVICE包含在此Intent中,因此注册一个BroadcastReceiver 来处理广播。

//新建一个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 中注册
@Override
protected void onCreate(Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //注册
        registerReceiver(receiver,getIntentFilter());
}

不要忘记在onDestroy()中进行反注销

unregisterReceiver(receiver);

执行设备搜索的操作是一项很繁重的任务,会消耗大量的资源。一旦你找到了一个设备并要进行连接,请务必确认是否停止搜索设备的操作。如果已经进行了连接,那么搜索操作将会显著地降低连接的速率,因此你应当在连接时停止搜索。可通过cancelDiscovery()方法停止搜索。

配对连接

要在两台设备之间创建连接,其中一台设备要作为服务器端,保持开放的BluetoothServerSocket并在线程中调用 accept()开始侦听连接请求,
而另一台设备必须利用扫描得到的服务端MAC发起连接请求。

:如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框

  • 服务器端:
  1. 通过调用listenUsingRfcommWithServiceRecord(String, UUID)获取BluetoothServerSocket
  2. 通过在run()中调用accept(),开始监听连接请求。
    由于accept()为阻塞调用,所以需要一个专门的线程进行连接的操作。
//用于接收连接请求,并启动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;
        // 在没有连接上的时候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:
                            // 准备通信
                            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为标记当前状态的变量,规定为

STATE_NONE = 0;       // 初始状态
STATE_LISTEN = 1;     // 等待连接
STATE_CONNECTING = 2; // 正在连接
STATE_CONNECTED = 3;  // 已经连接上设备
  • 客户端:
  1. 利用扫描到的服务器端的MAC地址得到远程设备
    BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(macAddress);
  2. 该远程设备调用方法createRfcommSocketToServiceRecord(UUID)建立安全连接。

注:UUID定义为00001101-0000-1000-8000-00805F9B34FB,为手机蓝牙串口的统一UUID。

  1. run()中通过调用conenct建立连接
    由于connect()为阻塞调用,因此该连接过程应始终在主 Activity 线程以外的线程中执行。
//用于蓝牙连接
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;
        }
        //调用外部类方法,启动用于通信线程connectedThread
        connected(mmSocket);
    }

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

注:connected(mmSocket)是应用中的虚构方法,它将启动用于传输数据的线程。

//连接完成后启动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();
}

通信

在成功连接两台设备后,每台设备都会有一个已连接的 BluetoothSocket。利用 BluetoothSocket传输任意数据的一般过程非常简单:

  1. 获取 InputStreamOutputStream,二者分别通过套接字
    以及 getInputStream()getOutputStream()来处理数据传输。
  2. 使用 read(byte[])write(byte[])读取数据并写入到流式传输。

因为read(byte[]) 和 write(byte[])方法都是阻塞调用的,所以需要一个专门的线程进行读写的操作。

//蓝牙连接完成后进行输入输出
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");
            //当连接状态为连接时,循环读取
            while(mState == STATE_CONNECTED){
                  try {
                        // 从InputStream中读取
                        Scanner in = new Scanner(mmInStream,"UTF-8");
                        String str = in.nextLine();
                        Log.i(TAG,"read: "+str);
                        //利用handle传递数据,此时为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);
            }
      }
}
  • 调用mConnectedThread.write(byte[] buffer)进行输出。
  • 输入流则在run()中被循环读取,这里采用了Handler处理数据传递。
    Constants的常量代表了对Message不同的处理方式,在ConnectedThreadrun()中,使用不同的 Constants值,调整输入流的处理方式。

例子中使用了
mHandler.obtainMessage(Constants.MESSAGE_TOAST);
代表把得到的数据以MESSAGE_TOAST的方式处理。

//利用Handler传递数据
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 公司的项目最近需要用到蓝牙开发的相关内容,因此特地查阅了Google官方文档的内容并进行二次整理,希望能对需要学习...
    Chuckiefan阅读 32,410评论 44 123
  • 1.简介 通过蓝牙API,可以实现以下内容: 扫描其他蓝牙设备 查询配对蓝牙设备的本地蓝牙适配器 创建RFCOMM...
    justCode_阅读 4,800评论 0 3
  • 蓝牙开发相关 使用Android Bluetooth APIs将设备通过蓝牙连接并通信,设置蓝牙,查找蓝牙设备,配...
    CoderMiner阅读 13,055评论 3 31
  • 今天跟媳妇吵架了,心情不好,所以想看个电影,到网上搜搜看,真的感觉没什么好看的电影,也许是因为心情不好的缘故...
    孩子的奶爸阅读 185评论 0 0
  • 今天给大家聊一个轻松的事: 其实社群营销在过去很长时间都一直存在,比如民国时间的奇女子林徽因,她就在做社群。 林徽...
    威乐河湾阅读 332评论 0 0