我们的程序或者说是软件,想要在本地设备上使用BLE通讯,首先要有使用BLE装置的权限,所以要在代码中获取权限在AndroidManifest.xml文件中,添加如下代码,以获取BLE权限:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>现对上述代码做出解释: 第一行的uses-feature后面的require是true,意思是只可以在支持BLE通讯的设备上运行此程序,也就是说如果程序能运行,就默认了BLE是可以支持的。 其他的permission都是一些基本的权限,百度即可。
到这里权限虽然是获取到了,但是在运行程序的时候,仍然要在主活动的**onCreate()**方法中,增加如下代码,来确保程序可以正常运行。
Android程序的主活动标注在AndroidManifest.xml文件中:
<activity android:name=".DeviceScanActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity>在这里我们看,标签内部有标签,这个“意图过滤器”,内部的就是过滤的内容,也是intent的参数,说白了就是:如果一个intent意图,它的意图的参数action和category和我过滤设置的一样,我就可以响应这个意图。 再回头看这段代码,这个意图过滤器过滤的参数设置其实就是用户在本地点开图标,打开应用的意图,所以当点开图标,打开应用这一刻,这个意图就被我们过滤器接收到,过滤器所属的activity就会响应。 响应的方式是:找到name对应的Activity类,实例化这个对象,并且执行onCreate()方法。 我们也就称响应点开图标,打开应用这个意图的类称为主活动。 我这里的主活动名字是:DeviceScanActivity.java
弄明白主活动,我们就可以在主活动的**onCreate()**方法中添加代码了:
// Use this check to determine whether BLE is supported on the device. Then you can // selectively disable BLE-related features. 应用适用于不支持ble的设备, if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }之所以在主活动的onCreate方法中添加,就是要在程序启动开始,就能够运行这段代码,来保证Android系统可以正常运行你的程序。(具体可以百度)
到此,BLE的权限就获取完成了,下一步该设置BLE了,也就是“应用如何打开蓝牙?”
正常情况下,我们的程序应该先确认设备的BLE是否可用。 如果前面设置了中的require:true的话,name就不需要检查了,因为打开这个应用已经默认设备是支持的了。
打开蓝牙我们首先需要操作BluetoothAdapter这个类,用这个类的实例对象来获取本地蓝牙适配器,核心语句如下,写在主活动的onCreate()方法内:
private BluetoothAdapter mbluetoothAdapter; ... // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); mbluetoothAdapter = bluetoothManager.getAdapter();现在mbluetoothAdapter这个实例化对象,就相当于本地的蓝牙了。
接下来的执行逻辑是:先判断适配器是否为空,若为空则代表本地蓝牙不可用,就弹出一个提示Toast,然后finish退出即可,若不为空则: 判断本地蓝牙是否开启,若开启则默认跳过即可,若没开启则startActivityForResult传入“系统级”intent,并且根据用户的选择1 开启,2 不开启 做出onActivityResult不同的响应,开启则跳过,不开启则finish结束程序。
先来 判断适配器是否为空,这个直接在onCreate方法中写入如下代码:
// Checks if Bluetooth is supported on the device. if (mBluetoothAdapter == null) { Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show(); finish(); return; }然后 判断本地蓝牙是否开启,这里不选择在onCreate方法中写入,选择在onResume方法中写如如下代码:
@Override protected void onResume() { super.onResume(); /** * 确保蓝牙打开,如果没打开, * 弹出一个intent 对话框dialog,提示是否打开蓝牙。 */ // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled, // fire an intent to display a dialog asking the user to grant permission to enable it. if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); // 弹出一个对话框,由于传入的intent对象是 “系统级”的 打开蓝牙的“系统意图” startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }这里的意思是,如果蓝牙没开启,则new一个intent为系统开启蓝牙,然后把这个intent传入startActivityForResult中去,执行,并且带着 请求表示码REQUEST_ENABLE_BT,这个是自己自定义即可,用来标注这次的活动,将来在onActivityResult方法中用来对这个活动的用户选择(result)做出唯一响应。 这段代码的运行结果是:弹出一个对话框,询问是否允许开启蓝牙,用户可以选择开启,也可以选择不开启。那选择的结果就在下面onActivityResult方法中做出具体响应。 这个onActivityResult方法是与上面的startActivityForResult方法绑定执行的,必须要有。
在onActivityResult中写入如下代码:
/** * 这个方法,就是配合startActivityForResult方法 * 因为startActivityForResult方法会让用户做出选择,而做出的选择对应不同的结果 * @param requestCode 是请求标识码,自定义即可,来自startActivityForResult,用来标注是哪里发的请求 * @param resultCode 是结果标识码,系统一般默认,或者响应方的setResult方法传入 * @param data 是响应的具体动作,Intent类型的对象 */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { // User chose not to enable Bluetooth. // 用户选择不启动蓝牙,则完成活动(关闭程序) if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) { finish(); return; } // super.onActivityResult(requestCode, resultCode, data); }可以看到,如果用户针对上次start的请求做出的回应是“取消开启蓝牙”(代码中的RESULT_CANCELED),那么就finish,结束程序。否则的话,用户选择的是“开启”蓝牙,那么就开启了蓝牙。 到此 设置BLE就结束了,该查询BLE设备了
这里的逻辑是,调用扫描的方法:mBluetoothAdapter.startLeScan方法,和mBluetoothAdapter.stopLeScan方法,分别对应扫描和停止,而方法需要传递一个参数,那就是“回调”的实例对象: mLeScanCallback,这是一个单独定义的 来自 系统自带的 BluetoothAdapter.LeScanCallback类的对象 其用于传递 BLE 扫描结果的界面。 而这个mLeScanCallback实际操作的是 一个mLeDeviceListAdapter对象,这个对象算是一个API类吧,继承自BaseAdapter类,主要是包含两个属性,如下代码所示:
private class LeDeviceListAdapter extends BaseAdapter { private ArrayList<BluetoothDevice> mLeDevices; private LayoutInflater mInflator; public LeDeviceListAdapter() { super(); mLeDevices = new ArrayList<BluetoothDevice>(); mInflator = DeviceScanActivity.this.getLayoutInflater(); } public void addDevice(BluetoothDevice device) { if(!mLeDevices.contains(device)) { mLeDevices.add(device); } } public BluetoothDevice getDevice(int position) { return mLeDevices.get(position); } public void clear() { mLeDevices.clear(); } @Override public int getCount() { return mLeDevices.size(); } @Override public Object getItem(int i) { return mLeDevices.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View view, ViewGroup viewGroup) { ViewHolder viewHolder; // General ListView optimization code. if (view == null) { view = mInflator.inflate(R.layout.listitem_device, null); viewHolder = new ViewHolder(); viewHolder.deviceAddress = (TextView) view.findViewById(R.id.device_address); viewHolder.deviceName = (TextView) view.findViewById(R.id.device_name); view.setTag(viewHolder); } else { viewHolder = (ViewHolder) view.getTag(); } BluetoothDevice device = mLeDevices.get(i); final String deviceName = device.getName(); if (deviceName != null && deviceName.length() > 0) viewHolder.deviceName.setText(deviceName); else viewHolder.deviceName.setText(R.string.unknown_device); viewHolder.deviceAddress.setText(device.getAddress()); return view; } }所以我们在最开始,在DevicesScanActivity 类内部,需要声明一个全局对象,后续还需要new一次:
public class DeviceScanActivity extends ListActivity { private LeDeviceListAdapter mLeDeviceListAdapter;上述代码中,涉及到View的操作,是额外的一个方法:
static class ViewHolder { TextView deviceName; TextView deviceAddress; }然后,在创建mLeScanCallback对象:
/** * Device scan callback. * 设备扫描 call back * 这里是BluetoothAdapter这个类中的 接口:LeScanCallback,类中的接口 * 用这个接口实例化一个对象 mLeScanCallback * 其用于传递 BLE 扫描结果的界面 */ private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { runOnUiThread(new Runnable() { @Override public void run() { mLeDeviceListAdapter.addDevice(device); mLeDeviceListAdapter.notifyDataSetChanged(); } }); } };这时候就创建好了这个 mLeScanCallback对象,就可以用来被扫描方法startLeScan()操作了,注意:这里不是传递参数,而是通过方法内部操作这个Callback对象,毕竟它是一个全局对象:
而扫描的时候,需要借助一个延时关闭的方法,来设置关闭扫描的条件 以下代码 用来 实现 扫描和停止功能:
/** * Activity for scanning and displaying available BLE devices. */ public class DeviceScanActivity extends ListActivity { private BluetoothAdapter bluetoothAdapter; private boolean mScanning; private Handler handler; // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; ... private void scanLeDevice(final boolean enable) { if (enable) { // Stops scanning after a pre-defined scan period. handler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } }, SCAN_PERIOD); mScanning = true; bluetoothAdapter.startLeScan(leScanCallback); } else { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } ... } ... }这个方法的调用写在onResume方法内,如下代码所示:
// Initializes list view adapter. // 初始化 列表 视图 适配器 mLeDeviceListAdapter = new LeDeviceListAdapter(); // 该方法创建一个显示的视图, setListAdapter(mLeDeviceListAdapter); scanLeDevice(true); }这里做一个提示: 对象是需要声明,然后再需要new的 变量声明就是new
现在解析这三行代码: 首先mLeDeviceListAdapter在上面是被声明过的,所以这里可以new它创建它,它包含一些方法和两个主要的属性:ArrayList类型的属性,和LayoutInflator类型的属性,这里再次把它们的代码展示:
private class LeDeviceListAdapter extends BaseAdapter { private ArrayList<BluetoothDevice> mLeDevices; private LayoutInflater mInflator; public LeDeviceListAdapter() { super(); mLeDevices = new ArrayList<BluetoothDevice>(); mInflator = DeviceScanActivity.this.getLayoutInflater(); }而且构造器已经对属性做了初始化;这个时候mLeScanCallback对象内部操作的就是这个mLeDeviceListAdapter对象了。 这一行如果注释掉的话,程序会闪退,打不开。
第二行:setListAdapter(mLeDeviceListAdapter);实际上是做了一个同步,做一个对应,这一步不能少。 这一行如果注释掉的话,程序会不扫描,或者说扫描没有结果。
第三行看似只是调用了扫描方法:但是内部必须有mLeScanCallback实例对象的支持;也就意味着有mLeDeviceListAdapter的支持与对应;
至此查询已经完成, 但是为了用户控制扫描的过程,需要onCreateOptionsMenu方法的重写,和onOptionsItemSelected方法的重写; 一个是创建一个Menu,一个是定义了用户的不同操作,不同的响应。
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); if (!mScanning) { menu.findItem(R.id.menu_stop).setVisible(false); menu.findItem(R.id.menu_scan).setVisible(true); menu.findItem(R.id.menu_refresh).setActionView(null); } else { menu.findItem(R.id.menu_stop).setVisible(true); menu.findItem(R.id.menu_scan).setVisible(false); menu.findItem(R.id.menu_refresh).setActionView( R.layout.actionbar_indeterminate_progress); } return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_scan: mLeDeviceListAdapter.clear(); scanLeDevice(true); break; case R.id.menu_stop: scanLeDevice(false); break; } return true; }至此,查询BLE设备就完成了。
连接BLE——读BLE设备的数据 的 执行逻辑是:
1.在搜索到设备的界面---------点击设备-----进入2 2.转到设备的自己的新界面,会显示设备的地址,设备的名称;此时未连接--------点击右上角“connect”按钮-----进入3 3.触发“点击connect”事件:mBluetoothLeService执行真正的连接方法: connect(mDeviceAddress), 与设备进行连接-----触发回调mGattCallback中的状态改变方法:onConnectionStateChange------进入4 4.状态改变方法onConnectionStateChange会执行广播------进入5;和触发“发现服务”(连接状态自动触发)方法:mBluetoothGatt.discoverServices()------进入6 5.发送广播,广播内容是intent,intent中是一个action=ACTION_GATT_CONNECTED,供另一边接收广播------进入7 6.触发onServicesDiscover方法,会发现这个设备支持的所有Service;然后会触发onServicesDiscovered方法,它会发送广播,发现服务”ACTION_GATT_SERVICES_DISCOVERED”,供另一边接收广播-----进入8 7.接收广播:这里是更新state显示:从disconnected变为connected;当然,能接收到广播是设置了广播过滤器。 8.接收广播:这里按照广播的内容,执行“展示所发现的服务”方法: displayGattServices(mBluetoothLeService.getSupportedGattServices())-----进入9 9.把发现的“服务”和“特征”制作成一个适配器,然后设置这个适配器为显示的视图提供数据:mGattServicesList.setAdapter(gattServiceAdapter) 此时:看到下拉菜单中的每一个特征,但是特征现在点击没有任何操作,需要对布局添加点击监听-----进入10 10.点击事件监听:点击任何一个特征,会出现以下两种情况之一:1是执行“读特征”方法mBluetoothLeService.readCharacteristic(characteristic)------进入11;2是执行“设置推送”方法 mBluetoothLeService.setCharacteristicNotification(characteristic, true)------进入12 11.这个“读特征”方法写在BluetoothLeService类中,读取结果通过“回调”mGattCallback来拿到,它内部有一个方法:onCharacteristicRead,当读特征触发时,这个方法会发送广播给控制类-------进入13 12.这个“设置推送”方法也在BluetoothLeService类中,功能为,设置“允许推送数据变化”,要配合这条语句才能达到,获得通知的目的:mBluetoothGatt.writeDescriptor(descriptor);当数据真的变化时,会触发“回调”中的onCharacteristicChanged方法-----进入15 13.这边广播接收器,接收到ACTION_DATA_AVAILABLE和带着的“特征”数据,会执行“展示数据”方法--------进入14 14.展示“特征”数据方法:mDataField.setText(data);吧“特征”数据,展示到视图的DateView位置显示。 15.“回调”中的onCharacteristicChanged方法相当于检测characteristic的变化,一旦发生数据变化,就会发送广播给控制类-----进入16 16.如13一样,广播接收器,接收到特征码,和数据,会去展示变化后的数据,展示到DataView中。