編輯:Android資訊
近期項目使用到了藍牙技術,菜鳥一枚,網上各種找資料,發現不是不全就是過時,要麼就是抄襲轉載,真實醉了,現在將這一塊的東西整理出來,供大家參考。
Android中的藍牙分兩種:經典藍牙、低功耗藍牙。
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 或 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
官方文檔:https://developer.android.com/guide/topics/connectivity/bluetooth-le.html?hl=zh-cn
// 如果為true表示只能在支持低功耗藍牙的設備上使用,如果不支持的設備也可以使用,采用代碼判斷 <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> // 代碼判斷 if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }
現在藍牙開發基本上都是低功耗藍牙,比如心率設備、耳機設備、手環,所以我們先從重要的開始,講講低功耗藍牙的使用,後面在完善經典藍牙。
聲明權限
創建項目在AndroidManifest.xml中聲明權限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lowett.android"> // 定位權限第二個包含第一個,所以這裡就聲明了一個,兩個都聲明也可以 <!-- <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 android:name="android.hardware.bluetooth_le" android:required="false"/> // 是有gps硬件,這個現在的智能手機沒有不支持的吧 <uses-feature android:name="android.hardware.location.gps"/> </manifest>
如果manifest中聲明的藍牙特性為false,那麼在代碼中監測是否支持BLE特性,
// 使用此檢查確定BLE是否支持在設備上,然後你可以有選擇性禁用BLE相關的功能 if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }
BluetoothAdapter
獲取BluetoothManager得到藍牙適配器BluetoothAdapter,注意這兩個類都是系統級別,只有一個,代表了你手機上的藍牙模塊
final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
如果獲取到的Adapter為空說明不支持藍牙,或者沒有藍牙模塊
開啟藍牙
前面獲取到了BluetoothAdapter,可以通過調用isEnabled()函數去檢測是否開啟了藍牙,false表示沒有開啟,如果沒有開啟需要去啟用,
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
代碼執行完後,會有彈框提示用戶是否啟用,我們需要在onActivityResult()中判斷返回
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == BluetoothLeManager.REQUEST_ENABLE_BT) { if (resultCode == Activity.RESULT_OK) { // something, 去掃描設備 startScan(); } else { new AlertDialog.Builder(this) .setMessage("請開啟藍牙,連接設備") .setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); // something } }) .create() .show(); } } }
掃描設備
低功耗藍牙和經典藍牙的掃描方式不同,如果熟悉經典藍牙,那就要可能掉進坑了起不來了,哈哈。藍牙掃描耗電比較嚴重,所以此處一定要記得在合適的實際停止掃描,比如定時停止、掃描到目標設備就停止(奇怪的是API為何不提供掃描時長的接口呢?)掃描時調用Adapter的startLeScan()方法,然而這個被標記為過時,API>=21被另一個取代。然後通過回掉得到掃描結果(經典藍牙是廣播)
public void startScan() { // 初始化一個handler initHandler(); if (!mScanning) { if (scanRunnable == null) { scanRunnable = new Runnable() { @Override public void run() { stopScan(); } }; } // SCAN_PERIOD = 3 * 10 * 1000, 30s後停止掃面 mHandler.postDelayed(scanRunnable, SCAN_PERIOD); // 新API // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // mBluetoothAdapter.getBluetoothLeScanner().startScan(this); // }else { // this 實現了BluetoothAdapter.LeScanCallback,即掃描結果回掉 mBluetoothAdapter.startLeScan(this); // } mScanning = true; Log.i(TAG, "開始掃描,藍牙設備"); } }
回調函數
@Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { // 得到掃描結果 Log.i(TAG, "掃描到的設備, name=" + device.getName() + ",address=" + device.toString()); }
注意:device:代表外設即目標設備 rssi:設一個強度值,但是時負值,利用這個值通過公式可以算出離你的距離
scanRecord:廣播數據,附加的數據,沒用到
停止掃描
掃描完成務必停止,因為掃描不僅耗電,還影響連接速度,所以當要連接的時候,先停止掃描時必須的
public void stopScan() { initHandler(); Log.i(TAG, "停止掃描,藍牙設備"); if (mScanning) { mScanning = false; // 開始掃描的接口,要一樣的不然停止不了 mBluetoothAdapter.stopLeScan(this); } if (scanRunnable != null) { mHandler.removeCallbacks(scanRunnable); scanRunnable = null; } }
連接設備
通常連接設備速度還是很快的,連接理論上來說也是無狀態的,所以也需要一個定式任務來,保證超時停止。
public boolean connect(Context context, String address) { if (mConnectionState == STATE_CONNECTED) { return false; } if (mBluetoothAdapter == null || TextUtils.isEmpty(address)) { return false; } initHandler(); if (connectRunnable == null) { connectRunnable = new Runnable() { @Override public void run() { mConnectionState = STATE_DISCONNECTING; disconnect(); } }; } // 30s沒反應停止連接 mHandler.postDelayed(connectRunnable, 30 * 1000); stopScan(); // 獲取到遠程設備, final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { return false; } // 開始連接,第二個參數表示是否需要自動連接,true設備靠近自動連接,第三個表示連接回調 mBluetoothGatt = device.connectGatt(context, false, mGattCallback); mBluetoothDeviceAddress = address; mConnectionState = STATE_CONNECTING; return true; }
監聽連接回調
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { // 連接狀態變化 @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { // 連接上 mConnectionState = STATE_CONNECTED; boolean success = mBluetoothGatt.discoverServices(); // 去發現服務 Log.i(TAG, "Attempting to start service discovery:" + success); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // 連接斷開 mConnectionState = STATE_DISCONNECTED; } } // 發現服務 @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.i(TAG,"發現服務"); // 解析服務 discoverService(); } } // 特征讀取變化 @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { } } // 收到數據 @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { mConnectionState = STATE_CONNECTED; } };
斷開連接
public void disconnect() { if (mBluetoothAdapter == null || mBluetoothGatt == null) { mConnectionState = STATE_DISCONNECTED; return; } // 連接成功的GATT mBluetoothGatt.disconnect(); mBluetoothGatt.close(); }
藍牙適配器沒有聽過連接多個設備的接口,需要我們自己實現,即獲取到目標設備的address後調用連接方法,自己維護多個BluetoothGatt即可(代碼稍後放出)。
正在做心率相關的藍牙設備,此處代碼發出來。
代碼還在完善中,僅供參考,穩定代碼將會發在Github中
package com.lowett.android.ble; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; import android.text.TextUtils; import com.fit.android.utils.Logger; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Locale; import java.util.UUID; /** * Email: [email protected] */ public class BluetoothLeManager implements BluetoothAdapter.LeScanCallback { public static final int REQUEST_ENABLE_BT = 1; private static final int SCAN_PERIOD = 3 * 10 * 1000; static final int STATE_DISCONNECTED = 1; public static final int STATE_CONNECTING = 2; public static final int STATE_DISCONNECTING = 3; public static final int STATE_CONNECTED = 4; public static final int STATE_DISCOVER_SERVICES = 5; @IntDef(value = {STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING, STATE_DISCOVER_SERVICES}) @Retention(RetentionPolicy.SOURCE) public @interface State { } public final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT); private static BluetoothLeManager ourInstance = new BluetoothLeManager(); // private Context mContext; private boolean is_inited = false; private android.bluetooth.BluetoothManager mBluetoothManager; private BluetoothAdapter mBluetoothAdapter; private String mBluetoothDeviceAddress; private int mConnectionState; private BluetoothGatt mBluetoothGatt; private boolean mScanning; private Runnable scanRunnable; private Handler mHandler; private Runnable connectRunnable; private OnDataReceivedListener mOnDataReceivedListener; // 記得清掉監聽 洩漏 private OnLeScanListener mOnLeScanListener; private OnConnectionStateChangeListener mOnConnectionStateChangeListener; private int retryCount; public static BluetoothLeManager getInstance() { return ourInstance; } private BluetoothLeManager() { } public void setOnDataReceivedListener(OnDataReceivedListener onDataReceivedListener) { mOnDataReceivedListener = onDataReceivedListener; } public interface OnConnectionStateChangeListener { void onConnectionStateChange(BluetoothGatt gatt, int status, int newState); void onConnectTimeout(); } public void setOnConnectionStateChangeListener(OnConnectionStateChangeListener onConnectionStateChangeListener) { mOnConnectionStateChangeListener = onConnectionStateChangeListener; } public interface OnLeScanListener { void onLeScan(BluetoothDevice device); } public interface OnDataReceivedListener { void onDataReceived(int heart); } private void init() { if (!is_inited) { is_inited = true; } } private boolean initialize(Context context) { init(); if (!is_inited) { throw new RuntimeException("請先調用init"); } if (mBluetoothAdapter != null) { return true; } // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothLeManager. if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { return false; } } mBluetoothAdapter = mBluetoothManager.getAdapter(); return mBluetoothAdapter != null; } public boolean isSupportBluetoothLe(Activity activity) { return activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); } public boolean isSupportBluetooth(Context context) { return initialize(context); } public void enableBluetooth(Activity activity) { if (!initialize(activity)) { return; } if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); } } public boolean isEnabled(Context context) { return initialize(context) && mBluetoothAdapter.isEnabled(); } private void initHandler() { if (mHandler == null) { mHandler = new Handler(Looper.getMainLooper()); } } public void startScan(OnLeScanListener onLeScanListener) { initHandler(); if (!mScanning) { if (scanRunnable == null) { scanRunnable = new Runnable() { @Override public void run() { stopScan(); } }; } mHandler.postDelayed(scanRunnable, SCAN_PERIOD); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // mBluetoothAdapter.getBluetoothLeScanner().startScan(this); // }else { mBluetoothAdapter.startLeScan(this); // } mScanning = true; this.mOnLeScanListener = onLeScanListener; Logger.i("開始掃描,藍牙設備"); } } @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { final BluetoothDevice tmp = device; Logger.i("掃描到的設備, name=" + device.getName() + ",address=" + device.toString()); if (mOnLeScanListener != null) { mHandler.post(new Runnable() { @Override public void run() { mOnLeScanListener.onLeScan(tmp); } }); } } public void stopScan() { initHandler(); mOnLeScanListener = null; Logger.i("停止掃描,藍牙設備"); if (mScanning) { mScanning = false; mBluetoothAdapter.stopLeScan(this); } if (scanRunnable != null) { mHandler.removeCallbacks(scanRunnable); scanRunnable = null; } } private void removeConnectRunnable() { if (connectRunnable != null) { mHandler.removeCallbacks(connectRunnable); connectRunnable = null; } } private void retry() { if (TextUtils.isEmpty(mBluetoothDeviceAddress)) { return; } mHandler.postDelayed(new Runnable() { @Override public void run() { if (++retryCount < 11 && mConnectionState < STATE_CONNECTED) { reconnect(retryCount); mHandler.postDelayed(this, retryCount * 5 * 1000); Logger.i("藍牙重試次數=" + retryCount); } } }, 2000); } private void reconnect(int count) { if ((mConnectionState >= STATE_CONNECTING)) { return; } if (connectRunnable == null) { connectRunnable = new Runnable() { @Override public void run() { mConnectionState = STATE_DISCONNECTING; disconnect(); } }; } mHandler.postDelayed(connectRunnable, count * 3 * 1000); if (mBluetoothDeviceAddress != null && mBluetoothGatt != null) { mBluetoothGatt.connect(); mConnectionState = STATE_CONNECTING; } } public boolean connect(Context context, String address) { if (mConnectionState == STATE_CONNECTED) { return false; } if (mBluetoothAdapter == null || TextUtils.isEmpty(address)) { return false; } initHandler(); if (connectRunnable == null) { connectRunnable = new Runnable() { @Override public void run() { mConnectionState = STATE_DISCONNECTING; disconnect(); if (mOnConnectionStateChangeListener != null) { mOnConnectionStateChangeListener.onConnectTimeout(); } } }; } mHandler.postDelayed(connectRunnable, 30 * 1000); stopScan(); // Previously connected device. Try to reconnect. if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { if (mBluetoothGatt.connect()) { mConnectionState = STATE_CONNECTING; return true; } else { return false; } } final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { return false; } // We want to directly connect to the device, so we are setting the autoConnect // parameter to false. mBluetoothGatt = device.connectGatt(context, false, mGattCallback); mBluetoothDeviceAddress = address; mConnectionState = STATE_CONNECTING; return true; } public void disconnect() { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Logger.i("BluetoothAdapter not initialized"); mConnectionState = STATE_DISCONNECTED; return; } mBluetoothGatt.disconnect(); } public void close() { disconnect(); if (mBluetoothGatt == null) { return; } mBluetoothGatt.close(); mBluetoothGatt = null; } public void disconnectNoRetry() { mBluetoothDeviceAddress = null; close(); } private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { mConnectionState = STATE_CONNECTED; boolean success = mBluetoothGatt.discoverServices(); Logger.i("Attempting to start service discovery:" + success); removeConnectRunnable(); Logger.i("鏈接上"); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { mConnectionState = STATE_DISCONNECTED; Logger.i("斷開鏈接"); retry(); } if (mOnConnectionStateChangeListener != null) { mOnConnectionStateChangeListener.onConnectionStateChange(gatt, status, newState); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Logger.i("發現服務"); discoverService(); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(characteristic); } } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { mConnectionState = STATE_CONNECTED; broadcastUpdate(characteristic); } }; /** * Retrieves a list of supported GATT services on the connected device. This should be * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. * * @return A {@code List} of supported services. */ private List<BluetoothGattService> getSupportedGattServices() { if (mBluetoothGatt == null) return null; return mBluetoothGatt.getServices(); } private void discoverService() { if (mConnectionState == STATE_DISCOVER_SERVICES) { return; } mConnectionState = STATE_DISCOVER_SERVICES; List<BluetoothGattService> list = getSupportedGattServices(); /** * BluetoothGattService = 00001800-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a00-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a01-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a04-0000-1000-8000-00805f9b34fb BluetoothGattService = 00001801-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a05-0000-1000-8000-00805f9b34fb 心跳服務 BluetoothGattService = 0000180d-0000-1000-8000-00805f9b34fb 心跳特征 BluetoothGattCharacteristic = 00002a37-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a38-0000-1000-8000-00805f9b34fb BluetoothGattService = 0000180f-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a19-0000-1000-8000-00805f9b34fb // 設備名字 BluetoothGattService = 0000180a-0000-1000-8000-00805f9b34fb BluetoothGattCharacteristic = 00002a28-0000-1000-8000-00805f9b34fb */ for (BluetoothGattService s : list) { if (!SampleGattAttributes.HEART_RATE_SERVICES.equals(s.getUuid().toString())) { continue; } final List<BluetoothGattCharacteristic> l = s.getCharacteristics(); for (final BluetoothGattCharacteristic bc : l) { if (!SampleGattAttributes.HEART_RATE_MEASUREMENT.equals(bc.getUuid().toString())) { continue; } Logger.i("連接藍牙 服務成功"); setCharacteristicNotification(bc, true); return; } } } private void broadcastUpdate(final BluetoothGattCharacteristic characteristic) { // This is special handling for the Heart Rate Measurement profile. Data parsing is // carried out as per profile specifications: // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { int flag = characteristic.getProperties(); int format = -1; if ((flag & 0x01) != 0) { format = BluetoothGattCharacteristic.FORMAT_UINT16; } else { format = BluetoothGattCharacteristic.FORMAT_UINT8; } int heartRate = characteristic.getIntValue(format, 1); Logger.i(String.format(Locale.getDefault(), "Received heart rate: %d", heartRate)); if (mOnDataReceivedListener != null) { mOnDataReceivedListener.onDataReceived(heartRate); } } // else { // For all other profiles, writes the data formatted in HEX. // final byte[] data = characteristic.getValue(); // if (data != null && data.length > 0) { // final StringBuilder stringBuilder = new StringBuilder(data.length); // for (byte byteChar : data) // stringBuilder.append(String.format("%02X ", byteChar)); // intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); // } // } /** * 2、 */ // sendBroadcast(intent); } public boolean isConnected() { return mConnectionState == STATE_CONNECTED; } public String getConnectedAddress() { if (!isConnected()) { return null; } return mBluetoothDeviceAddress; } public void readCharacteristic(BluetoothGattCharacteristic characteristic) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Logger.i("BluetoothAdapter not initialized"); return; } mBluetoothGatt.readCharacteristic(characteristic); } private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Logger.i("BluetoothAdapter not initialized"); return; } mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); // This is specific to Heart Rate Measurement. if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } } public void clear() { mOnLeScanListener = null; mOnConnectionStateChangeListener = null; } public void release() { // connectRunnable = null; // mHandler = null; // ourInstance = null; } }
項目還在進行中,遇到的問題將會持續拋出,完善此文檔。
Jenkins簡介 Jenkins是一個開源軟件項目,旨在提供一個開放易用的軟件平台,使軟件的持續集成變成可能。目前大部分公司都在使用Jenkins來持續構建。
軟件庫的存在使得Android編碼更方便快捷。在如此多 Android庫中,我們該如何尋找最合適的一款呢?下面我們做了一個列表供你參考。 動畫(Animatio
前言 無論你的應用多麼有創新性、有用,如果它卡得要命,或者非常消耗內存,那麼每人將會願意使用它。 因此,性能變得尤為重要。當你忙碌於構建精美的用戶界面或者完成新的
從 Android 5.0 Lollipop 開始提供一套 API 來支持嵌入的滑動效果。同樣在最新的 Support V4 包中也提供了前向的兼容。有了嵌入滑動