[desc]:添加蓝牙模块基础库

[author]:wangyimiao
master
yimiao 3 years ago
parent eb98dc0826
commit aaf76a2ab7

@ -52,8 +52,8 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation project(path: ':commonLib') implementation project(path: ':commonLib')

@ -6,7 +6,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.0' classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files

@ -43,8 +43,8 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'

@ -36,7 +36,8 @@ ext {
glide : "4.12.0", glide : "4.12.0",
photo_view : "2.3.0", photo_view : "2.3.0",
luban : "1.1.8", luban : "1.1.8",
kotlin_android : "1.4.1" kotlin_android : "1.4.1",
gson : "2.8.6",
] ]
dependencies = [ dependencies = [
@ -60,6 +61,7 @@ ext {
gilde_integration : "com.github.bumptech.glide:okhttp3-integration:${versions.glide}", gilde_integration : "com.github.bumptech.glide:okhttp3-integration:${versions.glide}",
annotationProcessor : "com.github.bumptech.glide:compiler:${versions.glide}", annotationProcessor : "com.github.bumptech.glide:compiler:${versions.glide}",
photo_view : "com.github.chrisbanes:PhotoView:${versions.photo_view}", photo_view : "com.github.chrisbanes:PhotoView:${versions.photo_view}",
luban : "top.zibin:Luban:${versions.luban}" luban : "top.zibin:Luban:${versions.luban}",
gson : "com.google.code.gson:gson${versions.gson}",
] ]
} }

@ -0,0 +1 @@
/build

@ -0,0 +1,42 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-parcelize'
}
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
viewBinding {
enabled = true
}
}
dependencies {
implementation project(path: ':commonLib')
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
// kotlin
implementation rootProject.ext.dependencies.kotlin
}

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.common.bluetooth">
<!-- 检测蓝牙状态 -->
<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-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:allowBackup="true">
<activity
android:name="com.common.bluetooth.BtDemoActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat"/>
<service
android:name="com.common.bluetooth.service.BLEReceiveService"
android:exported="true"
android:enabled="true">
</service>
<service
android:name="com.common.bluetooth.service.BLEClientService"
android:exported="true"
android:enabled="true">
</service>
</application>
</manifest>

@ -0,0 +1,99 @@
package com.common.bluetooth
import android.content.Context
import android.provider.Settings
import java.util.*
/**
* BT静态方法
*
* @author wangym
* @since 2021-10-14
*/
object BtConstants {
const val BT_NAME = "innovation bt"
/**
* 蓝牙类型
*/
enum class BLUETOOTH_TYPE {
/**
* 经典蓝牙
*/
CLASSIC,
/**
* 低功耗蓝牙
*/
BLE
}
/**
* 蓝牙类型
*/
enum class CLIENT_TYPE {
/**
* 服务端
*/
SERVER,
/**
* 客户端
*/
CLIENT
}
private const val STATUS_BASE = 0x1
const val CONNECT_SUCCESS = STATUS_BASE shl 1
const val CONNECT_ERROR = STATUS_BASE shl 2
const val READ_SUCCESS = STATUS_BASE shl 3
const val READ_ERROR = STATUS_BASE shl 4
const val WRITE_SUCCESS = STATUS_BASE shl 5
const val WRITE_ERROR = STATUS_BASE shl 6
const val CREATE_SERVER_SUCCESS = STATUS_BASE shl 7
const val CREATE_SERVER_ERROR = STATUS_BASE shl 8
const val DISCONNECT = STATUS_BASE shl 9
enum class EXCEPTION(s: String) {
NOT_CONNECTED("not connect"),
NULL_SERVICE("service is null"),
NULL_CHARACTERISTIC("characteristic is null"),
READ_EXCEPTION("characteristic read error"),
WRITE_EXCEPTION("characteristic write error"),
NOTIFY_OPEN_EXCEPTION("notification open error"),
NOTIFY_CLOSE_EXCEPTION("notification close error")
}
// Message types sent from the BluetoothChatService Handler
const val MESSAGE_STATE_CHANGE = 1
const val MESSAGE_READ = 2
const val MESSAGE_WRITE = 3
const val MESSAGE_DEVICE_NAME = 4
const val MESSAGE_TOAST = 5
// Key names received from the BluetoothChatService Handler
const val DEVICE_NAME = "device_name"
const val TOAST = "toast"
/**
* 最大的单包发送字节数
*/
const val MAX_MTU = 35
val UUID_SERVICE = UUID.fromString("66f564dc-121f-3e7f-80b1-f005d3f194c9")
val UUID_CHARACTERISTIC_NOTIFY = UUID.fromString("66f564dd-121f-3e7f-80b1-f005d3f194c9")
val UUID_CHARACTERISTIC_WRITE = UUID.fromString("66f564de-121f-3e7f-80b1-f005d3f194c9")
val UUID_DESCRIPTOR = UUID.fromString("66f564df-121f-3e7f-80b1-f005d3f194c9")
/**
* 获取UUID
*/
fun getUUid(context: Context?): UUID {
if (context == null) {
return UUID.randomUUID()
}
val androidId =
Settings.System.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
return UUID.nameUUIDFromBytes(androidId.toByteArray())
}
}

@ -0,0 +1,191 @@
package com.common.bluetooth;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import com.common.bluetooth.bean.AssociateEvent;
import com.common.bluetooth.bean.AssociateSourceEvent;
import com.common.bluetooth.bean.CommonMsg;
import com.common.bluetooth.bean.KeyboardEvent;
import com.common.bluetooth.callback.BLEClientListener;
import com.common.bluetooth.databinding.ActivityMainBinding;
import com.common.bluetooth.view.BtDeviceListAdapter;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Set;
/**
*
*
* @author wangym
* @since 2021-11-2
*/
public class BtDemoActivity extends AppCompatActivity {
private static final String TAG = "BtMainActivity";
/**
*
*/
private static final int REQUEST_ENABLE_BT = 0;
/**
*
*/
private static final int PERMISSION_REQUEST_LOCATION = 1;
private ActivityMainBinding mBinding = null;
private BtDeviceListAdapter btDeviceListAdapter = null;
private final ArrayList<BluetoothDevice> bluetoothDevices = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
initView();
checkBluetooth();
initData();
// 设置广播信息过滤
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);//每搜索到一个设备就会发送一个该广播
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//当全部搜索完后发送该广播
filter.setPriority(Integer.MAX_VALUE);//设置优先级
// 注册蓝牙搜索广播接收者,接收并处理搜索结果
this.registerReceiver(receiver, filter);
}
private void initData() {
bluetoothDevices.clear();
Set<BluetoothDevice> bondedDevices = BtManager.INSTANCE.getBoundDevices();
bluetoothDevices.addAll(bondedDevices);
btDeviceListAdapter = new BtDeviceListAdapter(this, bluetoothDevices);
LinearLayoutManager manager = new LinearLayoutManager(getApplicationContext());
mBinding.bleRv.setLayoutManager(manager);
mBinding.bleRv.setAdapter(btDeviceListAdapter);
btDeviceListAdapter.setOnDeviceClickListener(
index ->
{
BtManager.INSTANCE.connect(bluetoothDevices.get(index).getAddress());
BtManager.INSTANCE.setClientListener(new BLEClientListener() {
@Override
public void onResult(@NonNull CommonMsg result) {
Log.e("wangym", "onResult = " + result.getMsg());
runOnUiThread(
() -> Toast.makeText(BtDemoActivity.this, result.getMsg(),
Toast.LENGTH_SHORT).show());
}
@Override
public void onNotifyMsgReceive(@NonNull byte[] msg) {
Log.e("wangym", "notify = " + new String(msg));
runOnUiThread(
() -> Toast.makeText(BtDemoActivity.this, new String(msg),
Toast.LENGTH_SHORT).show());
}
});
});
}
private void initView() {
mBinding.searchBle.setOnClickListener(
l -> BtManager.INSTANCE.searchBtDevice(BtConstants.BLUETOOTH_TYPE.CLASSIC));
mBinding.sendMsg.setOnClickListener(l -> {
String value = "*" + mBinding.msgEt.getText().toString() + "#";
BtManager.INSTANCE.writeMsg(value);
});
mBinding.buildBleServer.setOnClickListener(v -> {
BtManager.INSTANCE.initServer(this, BtConstants.BLUETOOTH_TYPE.BLE);
BtManager.INSTANCE.setMsgReceiverListener(msg -> runOnUiThread(() -> {
Object event = BtUtils.INSTANCE.getEventFromSendMsg(msg);
if (event == null) {
mBinding.receiveContent.setText(new String(msg));
} else if (event instanceof KeyboardEvent) {
KeyboardEvent temp = (KeyboardEvent) event;
mBinding.receiveContent.setText(new Gson().toJson(temp));
} else if (event instanceof AssociateEvent) {
AssociateEvent temp = (AssociateEvent) event;
mBinding.receiveContent.setText(new Gson().toJson(temp));
} else if (event instanceof AssociateSourceEvent) {
AssociateSourceEvent temp = (AssociateSourceEvent) event;
mBinding.receiveContent.setText(new Gson().toJson(temp));
}
}));
});
}
private void checkBluetooth() {
BtManager.INSTANCE.initClient(this, BtConstants.BLUETOOTH_TYPE.BLE);
BtManager.INSTANCE.btPermissionCheck(this, PERMISSION_REQUEST_LOCATION);
}
/**
*
*/
private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "action = " + action);
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Discovery has found a device. Get the BluetoothDevice
// object and its info from the Intent.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
bluetoothDevices.add(device);
btDeviceListAdapter.notifyDataSetChanged();
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {//说明搜索已经完成
}
}
};
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) {
if (resultCode == RESULT_OK) {
Toast.makeText(this, "蓝牙打开成功", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "蓝牙打开失败", Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case PERMISSION_REQUEST_LOCATION:
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
finish();
}
break;
default:
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
BtManager.INSTANCE.release(this);
}
}

@ -0,0 +1,320 @@
package com.common.bluetooth
import android.Manifest
import android.app.Activity
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.util.Log.d
import android.util.Log.e
import androidx.core.content.ContextCompat.checkSelfPermission
import com.common.bluetooth.BtConstants.BLUETOOTH_TYPE
import com.common.bluetooth.adapter.BluetoothClientBLEAdapter
import com.common.bluetooth.bean.CommonMsg
import com.common.bluetooth.callback.BLEClientListener
import com.common.bluetooth.callback.BleMsgReceiverListener
import com.common.bluetooth.interfaces.IBluetoothClient
import com.common.bluetooth.service.BLEClientService
import com.common.bluetooth.service.BLEClientService.BLEClientBinder
import com.common.bluetooth.service.BLEReceiveService
import com.common.bluetooth.service.BLEReceiveService.ReceiverBinder
import com.common.bluetooth.service.BluetoothLeClient
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import java.nio.charset.Charset
import java.util.concurrent.atomic.AtomicBoolean
/**
* 蓝牙管理类
*
* @author wangym
* @since 2021-10-28
*/
object BtManager {
const val TAG = "BtManager"
/**
* 蓝牙客户端
*/
private var mBtClient: IBluetoothClient? = null
/**
* 客户端服务
*/
private var clientService: BLEClientService? = null
/**
* 服务端服务
*/
private var serverService: BLEReceiveService? = null
/**
* 消息接收监听
*/
var msgReceiverListener: BleMsgReceiverListener? = null
/**
* 蓝牙客户端监听
*/
var clientListener: BLEClientListener? = null
/**
* 是否绑定了服务端
*/
private var isBindServer: AtomicBoolean = AtomicBoolean(false)
/**
* 是否绑定了客户端
*/
private var isBindClient: AtomicBoolean = AtomicBoolean(false)
/**
* 当前使用的编码 默认是UTF-8
* 在传输前自动设置即可
*/
var curCharset = Charsets.UTF_8
/**
* 当前连接的设备MAC
*/
var curConnectMac = ""
/**
* 初始化蓝牙服务端模块
*/
fun initServer(context: Context, btType: BLUETOOTH_TYPE) {
mBtClient = BluetoothClientBLEAdapter(BluetoothLeClient.getInstance(context))
mBtClient!!.checkBluetoothDevice(btType)
initReceiverService(context)
}
/**
* 初始化蓝牙客户端模块
*/
fun initClient(context: Activity, btType: BLUETOOTH_TYPE) {
mBtClient = BluetoothClientBLEAdapter(BluetoothLeClient.getInstance(context))
mBtClient!!.checkBluetoothDevice(btType)
initClientService(context)
}
/**
* 初始化接收端服务
*/
private fun initReceiverService(context: Context) {
val intent = Intent(context, BLEReceiveService::class.java)
// 标志位BIND_AUTO_CREATE是的服务中onCreate得到执行,onStartCommand不会执行
context.bindService(intent, receiverConn, Context.BIND_AUTO_CREATE)
isBindServer.set(true)
}
/**
* 初始化客户端服务
*/
private fun initClientService(context: Context) {
val intent = Intent(context, BLEClientService::class.java)
// 标志位BIND_AUTO_CREATE是的服务中onCreate得到执行,onStartCommand不会执行
context.bindService(intent, clientConn, Context.BIND_AUTO_CREATE)
isBindClient.set(true)
}
private val receiverConn: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
serverService = (service as ReceiverBinder).service
serverService?.setMsgReceiveListener(object : BleMsgReceiverListener {
override fun onMsgReceive(msg: ByteArray) {
msgReceiverListener?.onMsgReceive(msg)
}
})
}
override fun onServiceDisconnected(name: ComponentName) {}
}
private val clientConn: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
clientService = (service as BLEClientBinder).service
clientService?.setClientListener(object : BLEClientListener {
override fun onResult(result: CommonMsg) {
// 如果连接成功,更新连接的设备
if (result.msgType == BtConstants.CONNECT_SUCCESS) {
curConnectMac = result.msg
} else if (result.msgType == BtConstants.DISCONNECT) {
curConnectMac = ""
}
clientListener?.onResult(result)
}
override fun onNotifyMsgReceive(msg: ByteArray) {
clientListener?.onNotifyMsgReceive(msg)
}
})
}
override fun onServiceDisconnected(name: ComponentName) {
clientService = null
clientListener?.onResult(CommonMsg(BtConstants.DISCONNECT, "disconnect from server"))
}
}
/**
* 扫描蓝牙设备
*
* @param btType 蓝牙类型
*/
fun searchBtDevice(btType: BLUETOOTH_TYPE) {
if (btType === BLUETOOTH_TYPE.CLASSIC) {
val mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (mBluetoothAdapter != null) {
if (mBluetoothAdapter.isDiscovering) {
d(TAG, "bt is discovering, cancel")
mBluetoothAdapter.cancelDiscovery()
}
d(TAG, "bt search start")
val startResult = mBluetoothAdapter.startDiscovery()
d(TAG, "start search = $startResult")
}
} else if (btType === BLUETOOTH_TYPE.BLE) {
d(TAG, "ble search start")
mBtClient!!.search(3000, true)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<BluetoothDevice> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(bleDevice: BluetoothDevice) {
d(TAG, "ble onScanResult")
}
override fun onError(e: Throwable) {
d(TAG, "ble onScanFailed")
}
override fun onComplete() {}
})
}
}
/**
* 连接设备BLE的连接方式
*
* @param mac MAC地址
*/
fun connect(mac: String) {
if (clientService == null) {
e(TAG, "writeMsg pls init first")
} else {
clientService!!.connect(mac, true)
}
}
/**
* 连接设备BLE的连接方式
*
* @param mac MAC地址
*/
fun connect(mac: String, enableNotify: Boolean) {
if (clientService == null) {
e(TAG, "writeMsg pls init first")
} else {
clientService!!.connect(mac, enableNotify)
}
}
/**
* 传输内容
*
* @param msg 传输的内容
*/
fun writeMsg(msg: String) {
d(TAG, "writeMsg $msg")
write(msg.toByteArray(curCharset))
}
/**
* 传输内容
*
* @param msg 传输的内容
*/
fun write(msg: ByteArray) {
if (clientService == null) {
e(TAG, "writeMsg pls init first")
} else {
clientService!!.write(msg)
}
}
/**
* 获取已配对的设备
*/
fun getBoundDevices(): Set<BluetoothDevice>? {
return mBtClient?.getBondedDevices()
}
/**
* 检查权限是否具备
*
* @param context 上下文 activity级
* @param requestCode 请求权限CODE
*/
fun btPermissionCheck(context: Activity, requestCode: Int) {
// 蓝牙6.0之后如果需要扫描需要添加获取位置权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
|| checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
context.requestPermissions(
arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
),
requestCode
)
}
}
}
/**
* 向远端发送通知
*
* @param msg 通知内容
*/
fun sendNotify(msg: String) {
d(TAG, "send notify : $msg")
sendNotify(msg.toByteArray(curCharset))
}
/**
* 向远端发送通知
*
* @param msg 通知内容
*/
fun sendNotify(msg: ByteArray) {
serverService?.sendNotify(msg)
}
/**
* 释放链接
*/
fun release(context: Context?) {
if (isBindServer.get()) {
context?.unbindService(receiverConn)
isBindServer.set(false)
}
if (isBindClient.get()) {
context?.unbindService(clientConn)
isBindClient.set(false)
}
}
}

@ -0,0 +1,354 @@
package com.common.bluetooth
import android.content.Context
import android.content.SharedPreferences
import android.text.TextUtils
import android.util.Log.d
import android.util.Log.e
import com.common.bluetooth.bean.*
/**
* 蓝牙工具类
*
* @author wangym
* @since 2021-10-27
*/
object BtUtils {
const val TAG = "BtUtils"
/**
* sp文件名称
*/
const val SP_NAME = "config"
/**
* MAC地址的KEY
*/
const val MAC_KEY = "mac"
enum class EVENT_TYPE(val type: String, val index: String) {
/**
* 点击事件
*/
EVENT_CLICK("click", "1"),
/**
* 拖动事件
*/
EVENT_DRAG("drag", "2"),
/**
* 模式切换事件
*/
EVENT_SWITCH("switch", "3")
}
/**
* 分割符号
*/
private const val SPLIT = '|'
/**
* 完整包开始标记
*/
private const val PACKAGE_FULL_START = '*'
/**
* 完整包结尾标记
*/
private const val PACKAGE_FULL_END = '#'
/**
* 键盘输入事件
*/
private const val INPUT_KEYBOARD = "1"
/**
* 交互事件
*/
private const val EVENT_INTERACTIVE = "1"
/**
* 联想词事件
*/
private const val EVENT_ASSOCIATE = "2"
/**
* 待联想词事件
*/
private const val EVENT_ASSOCIATE_SOURCE = "3"
/**
* 输入事件
*/
private const val EVENT_INPUT_SOURCE = "4"
/**
* 符号英文
*/
const val MOD_F_EN = "1"
/**
* 数字英文
*/
const val MOD_N_EN = "2"
/**
* 输入英文
*/
const val MOD_I_EN = "3"
/**
* 符号中文
*/
const val MOD_F_CN = "4"
/**
* 数字中文
*/
const val MOD_N_CN = "5"
/**
* 输入中文
*/
const val MOD_I_CN = "6"
/**
* 中间包
*/
private var tempMsg: ByteArray? = null
/**
* 将交互事件转化为传输的数据
* 第1位 包起始
* 第2位 输入类型 1键盘 2鼠标
* 第3位 事件分类 1交互事件 2联想词 3:待联想词 4:输入事件
* 第4位 事件类型 1click 2drag 3switch
* 第5位开始 事件内容
* 最后一位 包结束
*
* @param type 事件类型
* @param content 事件内容
*
* @return 实际传输的数据
*/
fun getSendMsgByEvent(type: EVENT_TYPE, content: String): String {
// 默认是 完整包 键盘事件 交互事件
var result = PACKAGE_FULL_START + INPUT_KEYBOARD + EVENT_INTERACTIVE
// 添加事件类型
result += type.index
// 添加事件内容
result += content
// 添加完整包结尾
result += PACKAGE_FULL_END
return result
}
/**
* 将待联想词事件转化为传输的数据
* 第1位 包起始
* 第2位 输入类型 1键盘 2鼠标
* 第3位 事件分类 1交互事件 2联想词 3:待联想词 4:输入事件
* 第4位开始 事件内容
* 最后一位 包结束
*
* @param content 事件内容
*
* @return 实际传输的数据
*/
fun getSendMsgByAssociateSourceEvent(content: String): String {
// 默认是 完整包 键盘事件 待联想词事件
var result = PACKAGE_FULL_START + INPUT_KEYBOARD + EVENT_ASSOCIATE_SOURCE
// 添加事件内容
result += content
// 添加完整包结尾
result += PACKAGE_FULL_END
return result
}
/**
* 将待联想词事件转化为传输的数据
* 第1位 包起始
* 第2位 输入类型 1键盘 2鼠标
* 第3位 事件分类 1交互事件 2联想词 3:待联想词 4:输入事件
* 第4位开始 事件内容
* 最后一位 包结束
*
* @param content 事件内容
*
* @return 实际传输的数据
*/
fun getSendMsgByInputSourceEvent(content: String): String {
// 默认是 完整包 键盘事件 输入事件
var result = PACKAGE_FULL_START + INPUT_KEYBOARD + EVENT_INPUT_SOURCE
// 添加事件内容
result += content
// 添加完整包结尾
result += PACKAGE_FULL_END
return result
}
/**
* 将联想词事件转化为传输的数据
* 第1位 包起始
* 第2位 输入类型 1键盘 2鼠标
* 第3位 事件分类 1交互事件 2联想词 3:待联想词 4:输入事件
* 第4位开始 联想词列表
* 最后一位 包结束
*
* @param associateList 联想词列表
* @return 实际传输的数据
*/
fun getSendMsgByAssociateEvent(associateList: List<String>): String {
// 默认是 完整包 键盘事件 交互事件
var result = PACKAGE_FULL_START + INPUT_KEYBOARD + EVENT_ASSOCIATE
// 添加联想词
for (item in associateList.withIndex()) {
if (item.index == associateList.size - 1) {
result += item.value
} else {
result = result + item.value + SPLIT
}
}
result += PACKAGE_FULL_END
return result
}
/**
* 根据传输过来的数据转化成对应对象
*
* @param receiveMsg 接收的数据
*
* @return 接收到的数据对象
*/
fun getEventFromSendMsg(receiveMsg: ByteArray): Any? {
d(TAG, "getEventFromSendMsg get msg = ${String(receiveMsg)}")
val msg = String(receiveMsg)
// 判断是否是包的起始
if (msg[0] == PACKAGE_FULL_START) {
// 收到包起始,清理之前的中间包
tempMsg = null
// 检查是否是完整包
return if (msg[msg.lastIndex] == PACKAGE_FULL_END) {
// 完整包直接进行解析
val finalString = String(receiveMsg)
return analysisMsg(finalString.substring(1, finalString.lastIndex))
} else {
// 等待后续的分包
d(TAG, "getEventFromSendMsg wait packages1")
tempMsg = receiveMsg
null
}
} else if (msg[msg.lastIndex] == PACKAGE_FULL_END) {
// 拼接成完整包直接进行解析
tempMsg = byteArrayMerger(tempMsg!!, receiveMsg)
// 如果最终拼接出来的包是完整的包直接进行解析
return if (String(tempMsg!!)[0] == PACKAGE_FULL_START) {
val finalString = String(tempMsg!!)
return analysisMsg(finalString.substring(1, finalString.lastIndex))
} else {
// 存在包丢失的情况,直接丢弃
e(TAG, "got package lost ,ignore cur package")
null
}
} else {
// 等待后续的分包
tempMsg = byteArrayMerger(tempMsg!!, receiveMsg)
d(TAG, "getEventFromSendMsg wait packages2")
return null
}
}
/**
* 解析接收到的数据
*
* @param msg 数据
*
* @return 接收到数据的结构类型
*/
private fun analysisMsg(msg: String): Any? {
d(TAG, "start analysisMsg")
d(TAG, "analysisMsg = $msg")
if (TextUtils.isEmpty(msg)) {
e(TAG, "got error input event : null")
return null
}
if (msg[0].toString() != INPUT_KEYBOARD) {
e(TAG, "got error input event : ${msg[0]}")
return null
}
// 判断是否复合基本要求基本要求为3字节
if (msg.length < 3) {
e(TAG, "got error input event msg : $msg")
return null
}
// 判断事件分类
if (msg[1].toString() == EVENT_INTERACTIVE) {
// 交互事件
// 判断需要解析的内容是否正常交互事件最小长度为4字节
if (msg.length < 4) {
e(TAG, "got error input EVENT_INTERACTIVE : $msg")
return null
}
val keyEvent = KeyboardEvent()
val event = BaseEvent(msg[2].toString(), msg.substring(3))
keyEvent.event.add(event)
return keyEvent
} else if (msg[1].toString() == EVENT_ASSOCIATE) {
// 联想词事件
val assEvent = AssociateEvent()
val temp = msg.substring(2)
val list = temp.split(SPLIT)
for (item in list.withIndex()) {
val assItem = AssociateItem(item.value, item.index + 1)
assEvent.associate.add(assItem)
}
return assEvent
} else if (msg[1].toString() == EVENT_ASSOCIATE_SOURCE) {
// 待联想词事件
return AssociateSourceEvent(msg.substring(2))
} else if (msg[1].toString() == EVENT_INPUT_SOURCE) {
// 输入事件
return InputSourceEvent(msg.substring(2))
} else {
e(TAG, "got error msg")
return null
}
}
/**
* 保存连接的MAC
*
* @param mac 设备MAC
* @param context 上下文
*/
fun saveConnectMac(context: Context, mac: String) {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
val edit = sharedPreferences.edit()
edit.putString(MAC_KEY, mac)
edit.apply()
}
/**
* 获取连接的MAC
*
* @param context 上下文
* @return 保存的设备MAC
*/
fun getConnectMac(context: Context): String? {
val sharedPreferences: SharedPreferences =
context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
return sharedPreferences.getString(MAC_KEY, "")
}
/**
* 合并byte[]
*/
fun byteArrayMerger(bt1: ByteArray, bt2: ByteArray): ByteArray {
val bt3 = ByteArray(bt1.size + bt2.size)
System.arraycopy(bt1, 0, bt3, 0, bt1.size)
System.arraycopy(bt2, 0, bt3, bt1.size, bt2.size)
return bt3
}
}

@ -0,0 +1,270 @@
package com.common.bluetooth.adapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.le.ScanResult;
import android.os.HandlerThread;
import android.util.Log;
import androidx.annotation.NonNull;
import com.common.bluetooth.BtConstants;
import com.common.bluetooth.callback.BaseResultCallback;
import com.common.bluetooth.callback.BtScanCallBack;
import com.common.bluetooth.exception.BluetoothException;
import com.common.bluetooth.exception.BluetoothNotOpenException;
import com.common.bluetooth.exception.BluetoothSearchConflictException;
import com.common.bluetooth.exception.BluetoothWriteException;
import com.common.bluetooth.interfaces.IBluetoothClient;
import com.common.bluetooth.interfaces.IBluetoothSearch;
import com.common.bluetooth.service.BluetoothLeClient;
import com.common.bluetooth.service.BluetoothLeConnector;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothClientBLEAdapter implements IBluetoothClient {
private static final String TAG = "BluetoothClient";
private final BluetoothLeClient mClient;
/**
*
* adapter线使
*/
private volatile static BaseResultCallback<byte[]> notifyCallback = null;
public BluetoothClientBLEAdapter(BluetoothLeClient client) {
mClient = client;
HandlerThread workThread = new HandlerThread("bluetooth ble worker");
workThread.start();
}
@Override
public Observable<BluetoothDevice> search(final int millis, final boolean cancel) {
return Observable.create(new ObservableOnSubscribe<BluetoothDevice>() {
@Override
public void subscribe(@NonNull final ObservableEmitter<BluetoothDevice> emitter) {
IBluetoothSearch searcher = mClient.getBluetoothSearcher();
if (searcher.isScanning() && !cancel) {
emitter.onError(new BluetoothSearchConflictException("is searching now"));
return;
}
if (searcher.isScanning()) {
stopSearch();
}
mClient.getBluetoothSearcher()
.startScan(millis, new BtScanCallBack() {
private final Set<BluetoothDevice> devices = new HashSet<>();
@Override
public void onResult(ScanResult result) {
BluetoothDevice device = result.getDevice();
if (devices.contains(device)) {
return;
}
devices.add(device);
emitter.onNext(device);
}
@Override
public void onComplete() {
emitter.onComplete();
}
@Override
public void onError(String msg) {
emitter.onError(new BluetoothNotOpenException(msg));
}
});
}
});
}
@Override
public void stopSearch() {
mClient.getBluetoothSearcher().stopScan();
}
@NonNull
@Override
public Observable<String> connect(final String mac) {
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(@NonNull final ObservableEmitter<String> emitter) {
BluetoothLeConnector connector = mClient.getBluetoothLeConnector(mac);
connector.connect(new BluetoothLeConnector.OnConnectListener() {
@Override
public void onConnect() {
}
@Override
public void onDisconnect() {
}
@Override
public void onServiceDiscover() {
emitter.onNext(mac);
emitter.onComplete();
}
@Override
public void onError(String msg) {
}
});
}
});
}
@Override
public void disconnect(@NonNull String mac) {
mClient.getBluetoothLeConnector(mac).disconnect();
}
@Override
public Observable<String> write(@NonNull final String mac, @NonNull final UUID service,
@NonNull final UUID characteristic,
@NonNull final byte[] values) {
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(@NonNull final ObservableEmitter<String> emitter) {
BluetoothLeConnector connector = mClient.getBluetoothLeConnector(mac);
connector.setOnDataAvailableListener(new BluetoothLeConnector.OnDataAvailableListener() {
@Override
public void onCharacteristicRead(byte[] values, int status) {
}
@Override
public void onCharacteristicChange(UUID characteristic, byte[] values) {
if (notifyCallback != null) {
notifyCallback.onSuccess(values);
}
}
@Override
public void onCharacteristicWrite(UUID characteristic, int status) {
emitter.onNext(mac);
emitter.onComplete();
}
@Override
public void onDescriptorWrite(UUID descriptor, int status) {
}
@Override
public void onError(BtConstants.EXCEPTION msg) {
Log.e(TAG, "write got error, msg = " + msg);
emitter.onError(new Throwable(msg.name()));
}
});
connector.writeCharacteristic(service, characteristic, values);
}
});
}
public void setNotifyCallback(BaseResultCallback<byte[]> notifyCallback) {
this.notifyCallback = notifyCallback;
}
@Override
public Observable<String> registerNotify(@NonNull final String mac, @NonNull final UUID service,
@NonNull final UUID characteristic,
final BaseResultCallback<byte[]> callback) {
return Observable.create(new ObservableOnSubscribe<String>() {
@Override
public void subscribe(@NonNull final ObservableEmitter<String> emitter) {
notifyCallback = callback;
BluetoothLeConnector connector = mClient.getBluetoothLeConnector(mac);
connector.setOnDataAvailableListener(new BluetoothLeConnector.OnDataAvailableListener() {
@Override
public void onCharacteristicRead(byte[] values, int status) {
}
@Override
public void onCharacteristicChange(UUID characteristic, byte[] values) {
if (notifyCallback != null) {
notifyCallback.onSuccess(values);
}
}
@Override
public void onCharacteristicWrite(UUID cha, int status) {
}
@Override
public void onDescriptorWrite(UUID descriptor, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "registerNotify pass");
emitter.onNext(mac);
emitter.onComplete();
} else {
String err = "write exception mac " + mac + " with " + status;
Log.e(TAG, err);
emitter.onError(new BluetoothWriteException(err, mac));
}
}
@Override
public void onError(BtConstants.EXCEPTION msg) {
emitter.onError(new BluetoothException(msg.name()));
}
});
connector.setCharacteristicNotification(service, characteristic, true);
}
});
}
@Override
public Observable<String> unRegisterNotify(@NonNull String mac, @NonNull UUID service,
@NonNull UUID characteristic) {
return null;
}
@Override
public void clean(@NonNull String mac) {
mClient.cleanConnector(mac);
}
@Override
public void cleanAll() {
mClient.cleanAllConnector();
}
@Override
public boolean checkBluetoothDevice(@NonNull BtConstants.BLUETOOTH_TYPE type) {
return mClient.checkBtDevice(type);
}
@Override
public void openBluetooth() {
mClient.openBt();
}
@Override
public boolean closeBluetooth() {
return mClient.closeBt();
}
@Override
public Set<BluetoothDevice> getBondedDevices() {
return mClient.getBondedDevices();
}
}

@ -0,0 +1,22 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 联想词事件
*
* @author wangym
* @since 2021-11-1
*/
@Parcelize
data class AssociateEvent(
/**
* 输入事件设备的类型 1键盘2鼠标
*/
val inputType: Int = 1,
/**
* 事件
*/
var associate: ArrayList<AssociateItem> = ArrayList()
) : Parcelable

@ -0,0 +1,22 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 联想词
*
* @author wangym
* @since 2021-11-1
*/
@Parcelize
data class AssociateItem(
/**
* 联想词
*/
var name: String = "",
/**
* 序列号
*/
var index: Int = 0
) : Parcelable

@ -0,0 +1,18 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 待联想词事件
*
* @author wangym
* @since 2021-11-1
*/
@Parcelize
data class AssociateSourceEvent(
/**
* 待联想词
*/
var associateSource: String = ""
) : Parcelable

@ -0,0 +1,19 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 基础事件类型
*/
@Parcelize
data class BaseEvent(
/**
* 事件类型
*/
var type: String = "",
/**
* 事件内容
*/
var data: String = ""
) : Parcelable

@ -0,0 +1,21 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 各类消息
* @author wangym
* @since 2021-11-1
*/
@Parcelize
data class CommonMsg(
/**
* 消息类型
*/
var msgType: Int,
/**
* 消息内容
*/
var msg: String
) : Parcelable

@ -0,0 +1,18 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 输入单个数字符号事件
*
* @author wangym
* @since 2021-11-1
*/
@Parcelize
data class InputSourceEvent(
/**
* 符号数字
*/
var inputSource: String = ""
) : Parcelable

@ -0,0 +1,19 @@
package com.common.bluetooth.bean
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 键盘事件
*/
@Parcelize
data class KeyboardEvent(
/**
* 输入事件设备的类型 1键盘2鼠标
*/
val inputType: Int = 1,
/**
* 事件
*/
var event: ArrayList<BaseEvent> = ArrayList()
) : Parcelable

@ -0,0 +1,19 @@
package com.common.bluetooth.callback
import com.common.bluetooth.bean.CommonMsg
/**
* 客户端监听
*/
interface BLEClientListener {
/**
* 结果
* @param result 回调
*/
fun onResult(result: CommonMsg)
/**
* 接收服务端通知
*/
fun onNotifyMsgReceive(msg: ByteArray)
}

@ -0,0 +1,24 @@
package com.common.bluetooth.callback;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public interface BaseResultCallback<T> {
/**
*
*
* @param data
*/
void onSuccess(T data);
/**
*
*
* @param msg
*/
void onFail(String msg);
}

@ -0,0 +1,13 @@
package com.common.bluetooth.callback
/**
* BLE数据接收监听
*/
interface BleMsgReceiverListener {
/**
* 接收的消息
*
* @param msg 传输的数据
*/
fun onMsgReceive(msg: ByteArray)
}

@ -0,0 +1,20 @@
package com.common.bluetooth.callback
/**
* 蓝牙链接监听
* @author wangym
* @since 2021-11-11
*/
interface BtConnectListener {
/**
* 链接成功
*/
fun onSuccess()
/**
* 链接失败
*
* @param error 错误信息
*/
fun onFail(error: String)
}

@ -0,0 +1,29 @@
package com.common.bluetooth.callback
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
/**
* 扫描结果回调
*
* @author wangym
* @since 2021-11-9
*/
open class BtScanCallBack : ScanCallback() {
/**
* 搜索结束
*/
open fun onComplete() {}
/**
* 搜索报错
* @param msg 错误信息
*/
open fun onError(msg: String?) {}
/**
* 搜索结果
* @param result 搜索结果
*/
open fun onResult(result: ScanResult?) {}
}

@ -0,0 +1,13 @@
package com.common.bluetooth.exception;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothException extends Exception {
public BluetoothException(String msg) {
super(msg);
}
}

@ -0,0 +1,13 @@
package com.common.bluetooth.exception;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothNotOpenException extends BluetoothException {
public BluetoothNotOpenException(String msg) {
super(msg);
}
}

@ -0,0 +1,13 @@
package com.common.bluetooth.exception;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothReadException extends BluetoothException {
public BluetoothReadException(String msg) {
super(msg);
}
}

@ -0,0 +1,13 @@
package com.common.bluetooth.exception;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothSearchConflictException extends BluetoothException {
public BluetoothSearchConflictException(String msg) {
super(msg);
}
}

@ -0,0 +1,20 @@
package com.common.bluetooth.exception;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothWriteException extends BluetoothException {
private String mac = "";
public BluetoothWriteException(String msg) {
super(msg);
}
public BluetoothWriteException(String msg, String mac) {
super(msg);
this.mac = mac;
}
}

@ -0,0 +1,120 @@
package com.common.bluetooth.interfaces
import android.bluetooth.BluetoothDevice
import com.common.bluetooth.BtConstants.BLUETOOTH_TYPE
import com.common.bluetooth.callback.BaseResultCallback
import io.reactivex.Observable
import java.util.*
/**
* 蓝牙客户端接口
* 蓝牙控制类. 使用这一个类连接蓝牙设备的时候最好在连接之前扫描一下附件的设备
* 如果能够扫描得到才进行连接降低连接蓝牙的出错率
*
* @author wangym
* @since 2021-11-9
*/
interface IBluetoothClient {
/**
* 打开蓝牙扫描操作. 如果此时正在扫描将会抛出正在扫描 [com.common.bluetooth.exception.BluetoothSearchConflictException] 错误
* 如果想强制中断当前扫描操作set cancel value to true.
*
* @param millis 扫描时间
* @param cancel 如果正在进行扫描操作设置是否中断当前扫描true 中断当前扫描操作
* false 如果当前正在进行扫描操作则会抛出 [com.common.bluetooth.exception.BluetoothSearchConflictException] 错误
* @return 扫描结果的列表无重复设备
*/
fun search(millis: Int, cancel: Boolean): Observable<BluetoothDevice>
/**
* 停止扫描
*/
fun stopSearch()
/**
* 连接一台蓝牙设备. 连接的蓝牙设备有最大限制
* 如果超出这一个数量即使连接上了蓝牙设备也扫描不到该设备的服务通道
*
* @param mac 需要连接蓝牙设备的地址
* @return 成功返回连接设备的地址
*/
fun connect(mac: String?): Observable<String>
/**
* 断开蓝牙连接, 释放蓝牙连接占用的蓝牙服务
*
* @param mac 需要断开连接的 mac 地址
*/
fun disconnect(mac: String)
/**
* 向一个蓝牙设备写入值
*
* @param mac 设备 mac 地址
* @param service 设备服务地址
* @param characteristic 设备 characteristic 地址
* @param values 需要写入的值
* @return 写入成功返回
*/
fun write(
mac: String, service: UUID, characteristic: UUID,
values: ByteArray
): Observable<String>?
/**
* 向蓝牙设备注册一个通道值改变的监听器,
* 每一个设备的每一个通道只允许同时存在一个监听器
*
* @param mac 需要监听的 mac 地址
* @param service 需要监听的设备的服务地址
* @param characteristic 需要监听设备的 characteristic
* @param callback 需要注册的监听器
* @return 成功或失败返回
*/
fun registerNotify(
mac: String, service: UUID, characteristic: UUID,
callback: BaseResultCallback<ByteArray>?
): Observable<String>
/**
* 解除在对应设备对应通道注册了的监听器
*
* @param mac 需要监听的 mac 地址
* @param service 需要监听的设备的服务地址
* @param characteristic 需要监听设备的 characteristic
*/
fun unRegisterNotify(mac: String, service: UUID, characteristic: UUID): Observable<String>?
/**
* 清空对应 MAC 地址的蓝牙设备缓存
*
* @param mac 蓝牙设备硬件地址
*/
fun clean(mac: String)
/**
* 清空所有缓存的蓝牙设备
*/
fun cleanAll()
/**
* 检查蓝牙设备是否支持
*/
fun checkBluetoothDevice(type: BLUETOOTH_TYPE): Boolean
/**
* 启动蓝牙
* 启动前优先检查蓝牙设备是否支持 [IBluetoothClient.checkBluetoothDevice] )}
*/
fun openBluetooth()
/**
* 关闭蓝牙
*/
fun closeBluetooth(): Boolean
/**
* 获取已绑定的蓝牙设备信息
*/
fun getBondedDevices(): Set<BluetoothDevice>?
}

@ -0,0 +1,10 @@
package com.common.bluetooth.interfaces
/**
* 蓝牙连接接口
*
* @author wangym
* @since 2021-11-9
*/
interface IBluetoothConnect {
}

@ -0,0 +1,31 @@
package com.common.bluetooth.interfaces
import com.common.bluetooth.callback.BtScanCallBack
/**
* 蓝牙搜索接口
*
* @author wangym
* @since 2021-10-19
*/
interface IBluetoothSearch {
/**
* 启动扫描
*
* 指定开始扫描蓝牙服务. 如果一个扫描服务正在运行,
* 马上停止当前的扫描服务, 只进行新的扫描服务.
*/
fun startScan(delayTime: Long, callBack: BtScanCallBack?)
/**
* 停止扫描
*/
fun stopScan()
/**
* 是否正在扫描
*
* @return 是否启动扫描成功
*/
fun isScanning(): Boolean
}

@ -0,0 +1,320 @@
package com.common.bluetooth.service;
import static com.common.bluetooth.BtConstants.CONNECT_SUCCESS;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.common.bluetooth.BtConstants;
import com.common.bluetooth.BtUtils;
import com.common.bluetooth.adapter.BluetoothClientBLEAdapter;
import com.common.bluetooth.bean.CommonMsg;
import com.common.bluetooth.callback.BLEClientListener;
import com.common.bluetooth.callback.BaseResultCallback;
import com.common.bluetooth.callback.BtConnectListener;
import com.common.bluetooth.interfaces.IBluetoothClient;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
/**
* BLE
*
* @author wangym
* @since 2021-10-29
*/
public class BLEClientService extends Service {
private static final String TAG = "BLEClientService";
/**
*
*/
private static final int WRITE_STATUS_IDLE = 1;
/**
*
*/
private static final int WRITE_STATUS_WRITING = 2;
/**
* BLE
*/
private IBluetoothClient mBtClient;
/**
* MAC
*/
private String connectMac;
/**
*
*/
private BLEClientListener clientListener;
/**
*
*/
private final LinkedBlockingQueue<byte[]> msgQueue = new LinkedBlockingQueue<>();
/**
*
*/
private final AtomicBoolean isEnableNotify = new AtomicBoolean(true);
/**
*
*/
private final AtomicInteger writeStatus = new AtomicInteger(WRITE_STATUS_IDLE);
@Override
public void onCreate() {
super.onCreate();
mBtClient = new BluetoothClientBLEAdapter(BluetoothLeClient.getInstance(this));
mBtClient.checkBluetoothDevice(BtConstants.BLUETOOTH_TYPE.BLE);
mBtClient.openBluetooth();
}
/**
*
*
* @param mac MAC
* @param enableNotify
*/
public void connect(String mac, boolean enableNotify) {
connect(mac, enableNotify, null);
}
/**
*
*/
private final BaseResultCallback<byte[]> notifyCallback = new BaseResultCallback<byte[]>() {
@Override
public void onSuccess(byte[] data) {
Log.d(TAG, "got notify onSuccess = " + new String(data));
if (clientListener != null) {
clientListener.onNotifyMsgReceive(data);
}
}
@Override
public void onFail(String msg) {
Log.d(TAG, "got notify onFail = " + msg);
}
};
/**
*
*
* @param mac MAC
* @param enableNotify
* @param connectListener
*/
public void connect(String mac, boolean enableNotify, BtConnectListener connectListener) {
this.connectMac = mac;
isEnableNotify.set(enableNotify);
if (TextUtils.isEmpty(connectMac)) {
connectMac = BtUtils.INSTANCE.getConnectMac(BLEClientService.this);
}
Observable<String>[] observables = new Observable[2];
observables[0] = mBtClient.connect(connectMac);
// 判断是否需要启动通知
if (enableNotify) {
observables[1] = mBtClient.registerNotify(connectMac, BtConstants.INSTANCE.getUUID_SERVICE(),
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_NOTIFY(), notifyCallback);
}
Observable.concatArray(observables).subscribe(new Observer<String>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
Log.d(TAG, "connect onSubscribe");
}
@Override
public void onNext(@NonNull String value) {
Log.d(TAG, String.format("connect onNext: %s", value));
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(TAG, "connect onError: ", e);
// 链接过程报错,断开链接
mBtClient.disconnect(connectMac);
connectMac = "";
if (clientListener != null) {
clientListener.onResult(new CommonMsg(BtConstants.CONNECT_ERROR, e.getMessage()));
}
if (connectListener != null) {
connectListener.onFail(e.getMessage());
}
}
@Override
public void onComplete() {
Log.d(TAG, "connect onComplete");
// 将链接成功的MAC地址保存
BtUtils.INSTANCE.saveConnectMac(BLEClientService.this, mac);
if (clientListener != null) {
clientListener.onResult(new CommonMsg(CONNECT_SUCCESS, mac));
}
if (connectListener != null) {
connectListener.onSuccess();
}
}
});
}
/**
*
*
* @param msg
*/
public void write(@NonNull final byte[] msg) {
if (mBtClient == null) {
Log.e(TAG, "write msg mBtClient got null");
return;
}
if (TextUtils.isEmpty(connectMac)) {
connectMac = BtUtils.INSTANCE.getConnectMac(BLEClientService.this);
if (TextUtils.isEmpty(connectMac)) {
Log.e(TAG, "write msg connectMac got null");
return;
}
}
Log.d(TAG, "add msg to queue : " + new String(msg));
// 将字符串添加到队列中
msgQueue.add(msg);
doTranslate();
}
/**
*
*/
private void doTranslate() {
// 判断状态
if (writeStatus.get() == WRITE_STATUS_WRITING) {
Log.d(TAG, "doTranslate is writing, return");
return;
}
synchronized (BLEClientService.class) {
writeStatus.set(WRITE_STATUS_WRITING);
// 从队列中获取数据
final byte[] msg = msgQueue.poll();
if (msg == null) {
Log.d(TAG, "doTranslate Queue is null");
writeStatus.set(WRITE_STATUS_IDLE);
return;
}
Log.d(TAG, "doTranslate start translate : " + new String(msg));
// 检查需要发送的内容是否超过了限额
int size = msg.length / BtConstants.MAX_MTU;
if (msg.length % BtConstants.MAX_MTU != 0) {
size++;
}
Observable<String>[] observables = new Observable[size];
for (int index = 0; index < size; index++) {
byte[] temp;
int arrSize;
if (index == size - 1) {
arrSize = msg.length - (index * BtConstants.MAX_MTU);
} else {
arrSize = BtConstants.MAX_MTU;
}
temp = new byte[arrSize];
System.arraycopy(msg, index * BtConstants.MAX_MTU, temp, 0, arrSize);
Observable<String> observable = mBtClient.write(connectMac, BtConstants.INSTANCE.getUUID_SERVICE(),
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_WRITE(),
temp);
observables[index] = observable;
}
Observable.concatArray(observables).subscribe(new Observer<String>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull String s) {
Log.d(TAG, String.format("write onNext %s", s));
}
@Override
public void onError(@NonNull Throwable e) {
Log.e(TAG, "write onError: " + e.getMessage());
if (BtConstants.EXCEPTION.NOT_CONNECTED.name().equals(e.getMessage())) {
// 写时发现,链接断开,则尝试重连
connect(connectMac, isEnableNotify.get(), new BtConnectListener() {
@Override
public void onSuccess() {
// 将数据重新放入队列后再次传输
msgQueue.add(msg);
writeStatus.set(WRITE_STATUS_IDLE);
doTranslate();
}
@Override
public void onFail(@NonNull String error) {
if (clientListener != null) {
clientListener.onResult(new CommonMsg(BtConstants.WRITE_ERROR, error));
}
}
});
} else {
if (clientListener != null) {
clientListener.onResult(new CommonMsg(BtConstants.WRITE_ERROR, e.getMessage()));
}
}
writeStatus.set(WRITE_STATUS_IDLE);
}
@Override
public void onComplete() {
Log.d(TAG, "write onComplete");
if (clientListener != null) {
clientListener.onResult(new CommonMsg(BtConstants.WRITE_SUCCESS, "write success"));
}
writeStatus.set(WRITE_STATUS_IDLE);
Log.d(TAG, "doTranslate end translate : " + msg);
doTranslate();
}
});
}
}
public void setClientListener(BLEClientListener clientListener) {
this.clientListener = clientListener;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new BLEClientBinder();
}
@Override
public boolean onUnbind(Intent intent) {
if (mBtClient != null && !TextUtils.isEmpty(connectMac)) {
mBtClient.disconnect(connectMac);
}
return super.onUnbind(intent);
}
public class BLEClientBinder extends Binder {
public BLEClientService getService() {
return BLEClientService.this;
}
}
}

@ -0,0 +1,282 @@
package com.common.bluetooth.service;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import com.common.bluetooth.BtConstants;
import com.common.bluetooth.callback.BleMsgReceiverListener;
import java.nio.charset.StandardCharsets;
/**
* BLE
*
* @author wangym
* @since 2021-10-29
*/
public class BLEReceiveService extends Service {
private static final String TAG = "BLEReceiveService";
/**
*
*/
private BluetoothManager bluetoothManager;
/**
* GATT
*/
private BluetoothGattServer gattServer;
/**
*
*/
private BluetoothGattCharacteristic mNotify;
/**
*
*/
private BleMsgReceiverListener receiverListener;
/**
*
*/
private BluetoothDevice remoteDevice;
private final BluetoothGattServerCallback serverCallback = new BluetoothGattServerCallback() {
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
super.onConnectionStateChange(device, status, newState);
Log.d(TAG,
"BluetoothGattServerCallback onConnectionStateChange status: " + status + " newState: " + newState);
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
super.onServiceAdded(status, service);
Log.d(TAG,
"BluetoothGattServerCallback onServiceAdded status: " + status + " service: " + service.getUuid().toString());
}
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
Log.d(TAG,
"BluetoothGattServerCallback onCharacteristicReadRequest characteristic: " + characteristic.getUuid().toString());
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, characteristic.getValue());
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
BluetoothGattCharacteristic characteristic, boolean preparedWrite,
boolean responseNeeded, int offset, byte[] value) {
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset,
value);
Log.d(TAG,
"BluetoothGattServerCallback onCharacteristicWriteRequest characteristic: " + characteristic.getUuid().toString());
Log.d(TAG,
"BluetoothGattServerCallback onCharacteristicWriteRequest value: " + new String(value));
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
// 处理响应内容
onResponseToClient(value, device, requestId);
}
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset,
BluetoothGattDescriptor descriptor) {
super.onDescriptorReadRequest(device, requestId, offset, descriptor);
Log.d(TAG, "onDescriptorReadRequest device = " + device.getAddress());
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
}
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor,
boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
String valueStr = new String(value);
Log.d(TAG, "onDescriptorWriteRequest device = " + device.getAddress());
Log.d(TAG, "onDescriptorWriteRequest valueStr = " + valueStr);
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
// 判断是否打开通知
if (new String(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE).equals(valueStr)) {
Log.d(TAG, "onDescriptorWriteRequest open notify");
mNotify = descriptor.getCharacteristic();
remoteDevice = device;
} else {
mNotify = null;
remoteDevice = null;
}
}
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
super.onExecuteWrite(device, requestId, execute);
Log.d(TAG, "onExecuteWrite device = " + device.getAddress());
}
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
super.onNotificationSent(device, status);
Log.d(TAG, "onNotificationSent device = " + device.getAddress());
}
};
@Override
public void onCreate() {
super.onCreate();
bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
if (bluetoothManager != null) {
Log.i(TAG, "initialize BluetoothManager");
initServer();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new ReceiverBinder();
}
public class ReceiverBinder extends Binder {
public BLEReceiveService getService() {
return BLEReceiveService.this;
}
}
public void setMsgReceiveListener(BleMsgReceiverListener listener) {
receiverListener = listener;
}
/**
*
*/
private void initServer() {
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)// 广播的模式 低功耗 平衡 低延迟
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(true)
.build();
AdvertiseData advertiseData = new AdvertiseData.Builder().build();
//通过UUID_SERVICE构建
AdvertiseData scanResponseData = new AdvertiseData.Builder()
.addServiceUuid(new ParcelUuid(BtConstants.INSTANCE.getUUID_SERVICE()))
.build();
//广播创建成功之后的回调
AdvertiseCallback callback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.d(TAG, "BLE advertisement added successfully");
//初始化服务
initServices(BLEReceiveService.this);
}
@Override
public void onStartFailure(int errorCode) {
Log.e(TAG, "Failed to add BLE advertisement, reason: " + errorCode);
}
};
//部分设备不支持Ble中心
BluetoothLeAdvertiser bluetoothLeAdvertiser = bluetoothManager.getAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser == null) {
Log.i(TAG, "BluetoothLeAdvertiser为null");
return;
}
//开始广播
bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponseData, callback);
}
private void initServices(Context context) {
// 创建gattServer服务器
gattServer = bluetoothManager.openGattServer(context, serverCallback);
// 创建指定的UUID的服务
BluetoothGattService service = new BluetoothGattService(BtConstants.INSTANCE.getUUID_SERVICE(),
BluetoothGattService.SERVICE_TYPE_PRIMARY);
//添加指定UUID的可读characteristic
BluetoothGattCharacteristic characteristicNotify = new BluetoothGattCharacteristic(
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_NOTIFY(),
BluetoothGattCharacteristic.PROPERTY_WRITE |
BluetoothGattCharacteristic.PROPERTY_READ |
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ);
//添加可读characteristic的descriptor
BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(BtConstants.INSTANCE.getUUID_DESCRIPTOR(),
BluetoothGattCharacteristic.PERMISSION_WRITE);
characteristicNotify.addDescriptor(descriptor);
service.addCharacteristic(characteristicNotify);
//添加指定UUID的可写characteristic
BluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_WRITE(),
BluetoothGattCharacteristic.PROPERTY_WRITE |
BluetoothGattCharacteristic.PROPERTY_READ |
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_WRITE);
service.addCharacteristic(characteristicWrite);
gattServer.addService(service);
}
/**
* 4.
*/
private void onResponseToClient(byte[] requestByte, BluetoothDevice device, int requestId) {
Log.d(TAG, String.format("onResponseToClientdevice name = %s, address = %s", device.getName(),
device.getAddress()));
Log.d(TAG, String.format("onResponseToClientrequestId = %s", requestId));
String str = new String(requestByte);
Log.d(TAG, "收到:" + str);
sendNotify(requestByte);
Log.d(TAG, "响应:" + str);
if (receiverListener != null) {
receiverListener.onMsgReceive(requestByte);
}
}
/**
*
*
* @param msg
*/
public void sendNotify(byte[] msg) {
Log.i(TAG, "sendNotify msg = " + new String(msg));
if (mNotify != null && gattServer != null && msg != null) {
Log.i(TAG, "---send notify start---");
mNotify.setValue(msg);
gattServer.notifyCharacteristicChanged(remoteDevice, mNotify, false);
Log.i(TAG, "---send notify end---");
} else {
Log.e(TAG, "got notify null");
}
}
@Override
public boolean onUnbind(Intent intent) {
mNotify = null;
return super.onUnbind(intent);
}
}

@ -0,0 +1,45 @@
package com.common.bluetooth.service;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import androidx.annotation.Nullable;
import com.common.bluetooth.callback.BtScanCallBack;
import com.common.bluetooth.interfaces.IBluetoothSearch;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothClassicSearcher implements IBluetoothSearch {
private static final String TAG = "BluetoothClassicSearcher";
private final Context mContext;
private final BluetoothAdapter mBluetoothAdapter;
public BluetoothClassicSearcher(Context mContext, BluetoothAdapter mBluetoothAdapter) {
this.mContext = mContext;
this.mBluetoothAdapter = mBluetoothAdapter;
}
@Override
public void stopScan() {
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
}
@Override
public boolean isScanning() {
return mBluetoothAdapter.isDiscovering();
}
@Override
public void startScan(long delayTime, @Nullable BtScanCallBack callBack) {
// 每次启动前,检查一下蓝牙是否已经在扫描之中
stopScan();
mBluetoothAdapter.startDiscovery();
}
}

@ -0,0 +1,519 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.common.bluetooth.service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import com.common.bluetooth.BtConstants;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
/**
* This class does all the work for setting up and managing Bluetooth
* connections with other devices. It has a thread that listens for
* incoming connections, a thread for connecting with a device, and a
* thread for performing data transmissions when connected.
*/
public class BluetoothConnectService {
// Debugging
private static final String TAG = "BluetoothConnectService";
// Name for the SDP record when creating server socket
private static final String NAME_SECURE = "BluetoothChatSecure";
private static final String NAME_INSECURE = "BluetoothChatInsecure";
// Unique UUID for this application
private final UUID CURRENT_UUID;
// Member fields
private final BluetoothAdapter mAdapter;
private final Handler mHandler;
private AcceptThread mSecureAcceptThread;
private AcceptThread mInsecureAcceptThread;
private ConnectThread mConnectThread;
private ConnectedThread mConnectedThread;
private int mState;
private int mNewState;
// Constants that indicate the current connection state
public static final int STATE_NONE = 0; // we're doing nothing
public static final int STATE_LISTEN = 1; // now listening for incoming connections
public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
public static final int STATE_CONNECTED = 3; // now connected to a remote device
/**
* Constructor. Prepares a new BluetoothChat session.
*
* @param context The UI Activity Context
* @param handler A Handler to send messages back to the UI Activity
*/
public BluetoothConnectService(Context context, Handler handler) {
CURRENT_UUID = BtConstants.INSTANCE.getUUid(context);
mAdapter = BluetoothAdapter.getDefaultAdapter();
mState = STATE_NONE;
mNewState = mState;
mHandler = handler;
}
/**
* Update UI title according to the current state of the chat connection
*/
private synchronized void updateUserInterfaceTitle() {
mState = getState();
Log.d(TAG, "updateUserInterfaceTitle() " + mNewState + " -> " + mState);
mNewState = mState;
// Give the new state to the Handler so the UI Activity can update
mHandler.obtainMessage(BtConstants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget();
}
/**
* Return the current connection state.
*/
public synchronized int getState() {
return mState;
}
/**
* Start the chat service. Specifically start AcceptThread to begin a
* session in listening (server) mode. Called by the Activity onResume()
*/
public synchronized void start() {
Log.d(TAG, "start");
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to listen on a BluetoothServerSocket
if (mSecureAcceptThread == null) {
mSecureAcceptThread = new AcceptThread(false);
mSecureAcceptThread.start();
}
if (mInsecureAcceptThread == null) {
mInsecureAcceptThread = new AcceptThread(false);
mInsecureAcceptThread.start();
}
// Update UI title
updateUserInterfaceTitle();
}
/**
* Start the ConnectThread to initiate a connection to a remote device.
*
* @param device The BluetoothDevice to connect
*/
public synchronized void connect(BluetoothDevice device) {
Log.d(TAG, "connect to: " + device);
// Cancel any thread attempting to make a connection
if (mState == STATE_CONNECTING) {
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to connect with the given device
mConnectThread = new ConnectThread(device, false);
mConnectThread.start();
// Update UI title
updateUserInterfaceTitle();
}
/**
* Start the ConnectedThread to begin managing a Bluetooth connection
*
* @param socket The BluetoothSocket on which the connection was made
* @param device The BluetoothDevice that has been connected
*/
public synchronized void connected(BluetoothSocket socket, BluetoothDevice
device, final String socketType) {
Log.d(TAG, "connected, Socket Type:" + socketType);
// Cancel the thread that completed the connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Cancel the accept thread because we only want to connect to one device
if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
}
if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
}
// Start the thread to manage the connection and perform transmissions
mConnectedThread = new ConnectedThread(socket, socketType);
mConnectedThread.start();
// Send the name of the connected device back to the UI Activity
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_DEVICE_NAME);
Bundle bundle = new Bundle();
bundle.putString(BtConstants.DEVICE_NAME, device.getName());
msg.setData(bundle);
mHandler.sendMessage(msg);
// Update UI title
updateUserInterfaceTitle();
}
/**
* Stop all threads
*/
public synchronized void stop() {
Log.d(TAG, "stop");
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
if (mSecureAcceptThread != null) {
mSecureAcceptThread.cancel();
mSecureAcceptThread = null;
}
if (mInsecureAcceptThread != null) {
mInsecureAcceptThread.cancel();
mInsecureAcceptThread = null;
}
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
}
/**
* Write to the ConnectedThread in an unsynchronized manner
*
* @param out The bytes to write
* @see ConnectedThread#write(byte[])
*/
public void write(byte[] out) {
// Create temporary object
ConnectedThread r;
// Synchronize a copy of the ConnectedThread
synchronized (this) {
if (mState != STATE_CONNECTED) return;
r = mConnectedThread;
}
// Perform the write unsynchronized
r.write(out);
}
/**
* Indicate that the connection attempt failed and notify the UI Activity.
*/
private void connectionFailed() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BtConstants.TOAST, "Unable to connect device");
msg.setData(bundle);
mHandler.sendMessage(msg);
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
// Start the service over to restart listening mode
BluetoothConnectService.this.start();
}
/**
* Indicate that the connection was lost and notify the UI Activity.
*/
private void connectionLost() {
// Send a failure message back to the Activity
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(BtConstants.TOAST, "Device connection was lost");
msg.setData(bundle);
mHandler.sendMessage(msg);
mState = STATE_NONE;
// Update UI title
updateUserInterfaceTitle();
// Start the service over to restart listening mode
BluetoothConnectService.this.start();
}
/**
* This thread runs while listening for incoming connections. It behaves
* like a server-side client. It runs until a connection is accepted
* (or until cancelled).
*/
private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
private String mSocketType;
public AcceptThread(boolean secure) {
BluetoothServerSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
// Create a new listening server socket
try {
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
CURRENT_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e);
}
mmServerSocket = tmp;
mState = STATE_LISTEN;
}
public void run() {
Log.d(TAG, "Socket Type: " + mSocketType +
"BEGIN mAcceptThread" + this);
setName("AcceptThread" + mSocketType);
BluetoothSocket socket = null;
// Listen to the server socket if we're not connected
while (mState != STATE_CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
break;
}
// If a connection was accepted
if (socket != null) {
synchronized (BluetoothConnectService.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// Situation normal. Start the connected thread.
connected(socket, socket.getRemoteDevice(),
mSocketType);
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, socket Type: " + mSocketType);
}
public void cancel() {
Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
}
}
}
/**
* This thread runs while attempting to make an outgoing connection
* with a device. It runs straight through; the connection either
* succeeds or fails.
*/
private class ConnectThread extends Thread {
private BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType;
public ConnectThread(BluetoothDevice device, boolean secure) {
mmDevice = device;
BluetoothSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
tmp = device.createRfcommSocketToServiceRecord(
CURRENT_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
}
mmSocket = tmp;
mState = STATE_CONNECTING;
}
public void run() {
Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
setName("ConnectThread" + mSocketType);
// Always cancel discovery because it will slow down a connection
mAdapter.cancelDiscovery();
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
mmSocket.connect();
} catch (IOException e) {
// Unable to connect; close the socket and return.
Log.e(TAG, e.toString());
try {
mmSocket.close();
} catch (IOException ie) {
connectionFailed();
}
return;
}
// Reset the ConnectThread because we're done
synchronized (BluetoothConnectService.this) {
mConnectThread = null;
}
// Start the connected thread
connected(mmSocket, mmDevice, mSocketType);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
}
}
}
/**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket, String socketType) {
Log.d(TAG, "create ConnectedThread: " + socketType);
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
mState = STATE_CONNECTED;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (mState == STATE_CONNECTED) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI Activity
mHandler.obtainMessage(BtConstants.MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "disconnected", e);
connectionLost();
break;
}
}
}
/**
* Write to the connected OutStream.
*
* @param buffer The bytes to write
*/
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
// Share the sent message back to the UI Activity
mHandler.obtainMessage(BtConstants.MESSAGE_WRITE, -1, -1, buffer)
.sendToTarget();
} catch (IOException e) {
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);
}
}
}
}

@ -0,0 +1,172 @@
package com.common.bluetooth.service;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import com.common.bluetooth.BtConstants;
import com.common.bluetooth.interfaces.IBluetoothSearch;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothLeClient {
@SuppressLint("StaticFieldLeak")
private static volatile BluetoothLeClient mInstance;
private final static String TAG = "BluetoothLeClient";
private final Context mContext;
private static Handler mBluetoothWorker;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothManager mBluetoothManager;
private IBluetoothSearch mBluetoothSearcher;
private final Map<String, BluetoothLeConnector> mGattConnectorMap
= new ConcurrentHashMap<>();
private BluetoothLeClient(Context context) {
mContext = context.getApplicationContext();
HandlerThread thread = new HandlerThread("bluetooth client worker");
thread.start();
mBluetoothWorker = new Handler(thread.getLooper());
}
public static BluetoothLeClient getInstance(Context context) {
if (mInstance == null) {
synchronized (BluetoothLeClient.class) {
if (mInstance == null) {
mInstance = new BluetoothLeClient(context);
}
}
}
return mInstance;
}
/**
*
*
* @param btType
*/
public boolean checkBtDevice(BtConstants.BLUETOOTH_TYPE btType) {
if (btType == BtConstants.BLUETOOTH_TYPE.BLE) {
// 检查当前手机是否支持ble 蓝牙
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.e(TAG, "not support ble device");
return false;
}
}
// For API level 18 and above, get a reference to BluetoothAdapter
// through BluetoothManager.
if (mBluetoothManager == null) {
mBluetoothManager = (BluetoothManager) mContext
.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
Log.e(TAG, "Unable to initialize BluetoothManager.");
return false;
}
}
if (mBluetoothAdapter == null) {
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
return false;
}
}
return true;
}
/**
* BluetoothAdapter
* Initializes a reference to the local Bluetooth adapter.
*
* @return Return true if the initialization is successful.
*/
public boolean openBt() {
if (mBluetoothAdapter == null) {
Log.e(TAG, "BluetoothManager do not init");
return false;
}
return mBluetoothAdapter.isEnabled() || mBluetoothAdapter.enable();
}
public boolean closeBt() {
if (mBluetoothAdapter == null) {
Log.e(TAG, "BluetoothManager do not init");
return false;
}
return mBluetoothAdapter.disable();
}
public IBluetoothSearch getBluetoothSearcher() {
if (mBluetoothSearcher == null) {
synchronized (BluetoothLeClient.class) {
if (mBluetoothSearcher == null) {
if (mBluetoothAdapter == null) {
String err = "cannot create BluetoothLeSearcher instance because not " +
"initialize, please call initialize() method";
Log.e(TAG, err);
return null;
}
mBluetoothSearcher = new BluetoothLeSearcher(mContext, mBluetoothAdapter, mBluetoothWorker);
}
}
}
return mBluetoothSearcher;
}
public BluetoothLeConnector getBluetoothLeConnector(String mac) {
BluetoothLeConnector result;
if ((result = mGattConnectorMap.get(mac)) != null) {
return result;
}
result = new BluetoothLeConnector(mContext, mBluetoothAdapter, mac, mBluetoothWorker);
mGattConnectorMap.put(mac, result);
return result;
}
public void cleanConnector(String mac) {
BluetoothLeConnector result;
if ((result = mGattConnectorMap.get(mac)) != null) {
mGattConnectorMap.remove(mac);
result.disconnect();
result.setOnDataAvailableListener(null);
}
}
/**
*
*
*/
public void cleanAllConnector() {
for (String mac : mGattConnectorMap.keySet()) {
cleanConnector(mac);
}
}
/**
*
*/
public Set<BluetoothDevice> getBondedDevices() {
return mBluetoothAdapter.getBondedDevices();
}
}

@ -0,0 +1,516 @@
package com.common.bluetooth.service;
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.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.util.Log;
import com.common.bluetooth.BtConstants;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import io.reactivex.functions.Consumer;
/**
* Service for managing connection and data communication with a GATT server
* hosted on a given Bluetooth LE device.
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothLeConnector {
private final static String TAG = "BluetoothLeConnector";
/**
*
*/
public interface OnConnectListener {
void onConnect();
void onDisconnect();
void onServiceDiscover();
void onError(String msg);
}
/**
*
*/
public interface OnDataAvailableListener {
void onCharacteristicRead(byte[] values, int status);
void onCharacteristicChange(UUID characteristic, byte[] values);
void onCharacteristicWrite(UUID characteristic, int status);
void onDescriptorWrite(UUID descriptor, int status);
void onError(BtConstants.EXCEPTION msg);
}
private final Context mContext;
private final BluetoothAdapter mBluetoothAdapter;
private final String mBluetoothDeviceAddress;
private final Handler mWorkHandler;
private final Handler mConnectHandler;
private BluetoothGatt mBluetoothGatt;
private OnConnectListener mOnConnectListener;
private OnDataAvailableListener mOnDataAvailableListener;
private final AtomicInteger mConnectStatus = new AtomicInteger(BluetoothGatt.STATE_DISCONNECTED);
private final AtomicBoolean mIsStartService = new AtomicBoolean(false);
private final AtomicLong mDisconnectTime = new AtomicLong(SystemClock.elapsedRealtime());
private final AtomicLong mConnectTime = new AtomicLong(SystemClock.elapsedRealtime());
private BluetoothGatt getBluetoothGatt() {
return mBluetoothGatt;
}
private void setBluetoothGatt(BluetoothGatt bluetoothGatt) {
this.mBluetoothGatt = bluetoothGatt;
}
private void setOnConnectListener(OnConnectListener l) {
mOnConnectListener = l;
}
/**
*
*/
public void setOnDataAvailableListener(OnDataAvailableListener l) {
mOnDataAvailableListener = l;
}
BluetoothLeConnector(Context context, BluetoothAdapter adapter, String mac, Handler worker) {
mContext = context.getApplicationContext();
mBluetoothAdapter = adapter;
mBluetoothDeviceAddress = mac;
mWorkHandler = worker;
HandlerThread thread = new HandlerThread("bluetooth connector");
thread.start();
mConnectHandler = new Handler(thread.getLooper());
}
/**
* Implements callback methods for GATT events that the app cares about. For
* example, connection change and services discovered.
* GATT
*/
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
final int newState) {
mWorkHandler.post(() -> {
Log.d(TAG, "onConnectionStateChange: thread "
+ Thread.currentThread() + " status " + newState);
// 清空连接初始化的超时连接任务代码
mConnectHandler.removeCallbacksAndMessages(null);
if (status != BluetoothGatt.GATT_SUCCESS) {
String err = String.format("Cannot connect device with error status: %s", status);
disconnectGatt();
Log.e(TAG, err);
mOnConnectListener.onError(err);
mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
return;
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
// setting connect status is connected
mConnectStatus.set(BluetoothGatt.STATE_CONNECTED);
mOnConnectListener.onConnect();
// Attempts to discover services after successful connection.
mIsStartService.set(false);
if (!gatt.discoverServices()) {
String err = "discover service return false";
Log.e(TAG, err);
gatt.disconnect();
mOnConnectListener.onError(err);
return;
}
// 解决连接 Service 过长的问题
// 有些手机第一次启动服务的时间大于 2s
mConnectHandler.postDelayed(() ->
mWorkHandler.post(() -> {
if (!mIsStartService.get()) {
gatt.disconnect();
}
}), 3000L);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
if (!mIsStartService.get()) {
String err = "service not found force disconnect";
Log.e(TAG, err);
mOnConnectListener.onError(err);
}
mOnConnectListener.onDisconnect();
close();
mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
}
});
}
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
mWorkHandler.post(() -> {
// 清空连接服务设置的超时回调
mIsStartService.set(true);
mConnectHandler.removeCallbacksAndMessages(null);
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "进入通道连接!!!! in thread " + Thread.currentThread());
mOnConnectListener.onServiceDiscover();
} else {
String err = "onServicesDiscovered received: " + status;
Log.e(TAG, err);
gatt.disconnect();
}
});
}
@Override
public void onCharacteristicRead(final BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic,
final int status) {
Log.d(TAG, "callback characteristic read status " + status
+ " in thread " + Thread.currentThread());
if (status == BluetoothGatt.GATT_SUCCESS && mOnDataAvailableListener != null) {
mOnDataAvailableListener.onCharacteristicRead(
characteristic.getValue(),
status);
}
}
@Override
public void onCharacteristicChanged(final BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "callback characteristic change in thread " + Thread.currentThread());
Log.d(TAG, "callback characteristic change value = " + new String(characteristic.getValue()));
if (mOnDataAvailableListener != null) {
mOnDataAvailableListener.onCharacteristicChange(
characteristic.getUuid(), characteristic.getValue());
}
}
@Override
public void onCharacteristicWrite(final BluetoothGatt gatt,
final BluetoothGattCharacteristic characteristic,
final int status) {
Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
if (mOnDataAvailableListener != null) {
mOnDataAvailableListener.onCharacteristicWrite(
characteristic.getUuid(), status);
}
}
@Override
public void onDescriptorWrite(final BluetoothGatt gatt,
final BluetoothGattDescriptor descriptor,
final int status) {
Log.d(TAG, "callback descriptor write in thread " + Thread.currentThread());
if (mOnDataAvailableListener != null) {
mOnDataAvailableListener.onDescriptorWrite(
descriptor.getUuid(), status);
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
Log.d(TAG, "callback donMtuChanged mtu = " + mtu + " status = " + status);
}
};
/**
* Connects to the GATT server hosted on the Bluetooth LE device.
*/
public void connect(final OnConnectListener callback) {
mWorkHandler.post(() -> {
Log.d(TAG, "connect: in thread " + Thread.currentThread());
if (mBluetoothAdapter == null) {
String err = "BluetoothAdapter not initialized or unspecified address.";
Log.e(TAG, err);
callback.onError(err);
return;
}
final BluetoothDevice device
= mBluetoothAdapter.getRemoteDevice(mBluetoothDeviceAddress);
if (device == null) {
String err = "Device not found. Unable to connect.";
Log.e(TAG, err);
callback.onError(err);
return;
}
// 需要连接的MAC已经在连接中
if (mConnectStatus.get() == BluetoothGatt.STATE_CONNECTED) {
Log.d(TAG, "Device is connected");
callback.onServiceDiscover();
return;
}
// 避免自动硬件断开后又自动连接,导致 service 回调被调用
// 这里有隐患,实践证明 close 方法是异步调用的且单例,
// 这就是说当一个 gatt 被创建之后,调用之前的 gatt 可能会把当前的 gatt close掉.
// 最终造成 gatt 泄漏问题.
// 一个解决方案就是延长连接硬件的时间
if (mConnectStatus.get() != BluetoothGatt.STATE_DISCONNECTED) {
String err = "Device is connecting";
Log.e(TAG, err);
callback.onError(err);
return;
}
// 检查完没有任何错误再设置回调,确保上一次没有完成的操作得以继续回调,而不是被新的回调覆盖
setOnConnectListener(callback);
// We want to directly connect to the device, so we are setting the
// autoConnect
// parameter to false.
Log.d(TAG, "Trying to create a new connection.");
mConnectStatus.set(BluetoothGatt.STATE_CONNECTING);
mIsStartService.set(false);
mConnectTime.set(SystemClock.elapsedRealtime());
setBluetoothGatt(device.connectGatt(mContext, false, mGattCallback));
if (getBluetoothGatt() == null) {
String err = "bluetooth is not open!";
Log.e(TAG, err);
mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
callback.onError(err);
return;
}
// 自定义MTU
// getBluetoothGatt().requestMtu(512);
// 开一个定时器,如果超出 20s 就强制断开连接
// 这个定时器必须在连接上设备之后清掉
mConnectHandler.removeCallbacksAndMessages(null);
mConnectHandler.postDelayed(() ->
mWorkHandler.post(() -> {
if (mConnectStatus.get() == BluetoothGatt.STATE_CONNECTING) {
disconnectGatt();
String err = "connect timeout, cannot not connect device";
Log.e(TAG, err);
callback.onError(err);
}
}), 20000L);
});
}
/**
* Disconnects an existing connection or cancel a pending connection. The
* disconnection result is reported asynchronously through the
* {@link BluetoothGattCallback#onConnectionStateChange(BluetoothGatt, int, int)}
* callback.
*/
public void disconnect() {
mWorkHandler.post(this::disconnectGatt);
}
private void disconnectGatt() {
Log.d(TAG, "disconnect: in thread " + Thread.currentThread());
if (mBluetoothAdapter == null || getBluetoothGatt() == null) {
Log.e(TAG, "BluetoothAdapter not initialized");
return;
}
if (mConnectStatus.get() == BluetoothGatt.STATE_DISCONNECTED) {
close();
return;
}
getBluetoothGatt().disconnect();
// 确保 Gatt 一定会被 close
if (mConnectStatus.get() == BluetoothGatt.STATE_CONNECTING) {
mConnectHandler.removeCallbacksAndMessages(null);
close();
}
}
/**
* After using a given BLE device, the app must call this method to ensure
* resources are released properly.
*/
private void close() {
Log.d(TAG, "close: in thread " + Thread.currentThread());
if (getBluetoothGatt() == null) {
Log.e(TAG, "BluetoothAdapter not initialized");
return;
}
getBluetoothGatt().close();
setBluetoothGatt(null);
mConnectStatus.set(BluetoothGatt.STATE_DISCONNECTED);
mDisconnectTime.set(SystemClock.elapsedRealtime());
}
private void callDataAvailableListenerError(BtConstants.EXCEPTION err) {
Log.e(TAG, err.toString());
if (mOnDataAvailableListener != null) {
mOnDataAvailableListener.onError(err);
}
}
private void checkChannelAndDo(UUID service,
UUID characteristic,
Consumer<BluetoothGattCharacteristic> action) {
if (mBluetoothAdapter == null || getBluetoothGatt() == null
|| mConnectStatus.get() != BluetoothGatt.STATE_CONNECTED) {
callDataAvailableListenerError(BtConstants.EXCEPTION.NOT_CONNECTED);
return;
}
BluetoothGattService serviceChanel = getBluetoothGatt().getService(service);
if (serviceChanel == null) {
callDataAvailableListenerError(BtConstants.EXCEPTION.NULL_SERVICE);
return;
}
BluetoothGattCharacteristic gattCharacteristic
= serviceChanel.getCharacteristic(characteristic);
if (characteristic == null) {
callDataAvailableListenerError(BtConstants.EXCEPTION.NULL_CHARACTERISTIC);
return;
}
try {
action.accept(gattCharacteristic);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* ,
* {@link BluetoothLeConnector#setOnDataAvailableListener(OnDataAvailableListener)}
*
*/
public void readCharacteristic(final UUID service, final UUID characteristic) {
mWorkHandler.post(() -> {
Log.d(TAG, "in readCharacteristic");
checkChannelAndDo(service, characteristic,
new Consumer<BluetoothGattCharacteristic>() {
@Override
public void accept(BluetoothGattCharacteristic bluetoothGattCharacteristic)
throws Exception {
if (getBluetoothGatt()
.readCharacteristic(bluetoothGattCharacteristic)) {
callDataAvailableListenerError(BtConstants.EXCEPTION.READ_EXCEPTION);
}
}
});
});
}
/**
* write something data to characteristic
*/
public void writeCharacteristic(final UUID service,
final UUID characteristic,
final byte[] values) {
mWorkHandler.post(() -> {
Log.d(TAG, "writing characteristic in thread " + Thread.currentThread());
checkChannelAndDo(service, characteristic,
bluetoothGattCharacteristic -> {
bluetoothGattCharacteristic.setValue(values);
if (!getBluetoothGatt()
.writeCharacteristic(bluetoothGattCharacteristic)) {
callDataAvailableListenerError(BtConstants.EXCEPTION.WRITE_EXCEPTION);
}
});
});
}
/**
*
*/
public void writeCharacteristic(UUID service, UUID characteristic, String values) {
writeCharacteristic(service, characteristic, values.getBytes());
}
/**
* UUID
* Enables or disables notification on a give characteristic.
*
* @param characteristic Characteristic to act on.
* @param enabled If true, enable notification. False otherwise.
*/
public void setCharacteristicNotification(final UUID service,
final UUID characteristic,
final boolean enabled) {
mWorkHandler.post(() -> checkChannelAndDo(service, characteristic, new Consumer<BluetoothGattCharacteristic>() {
@Override
public void accept(BluetoothGattCharacteristic gattCharacteristic) {
if (enabled) {
Log.i(TAG, "Enable Notification");
// 启动蓝牙通知
getBluetoothGatt().setCharacteristicNotification(gattCharacteristic, true);
// 修改描述设置为开启蓝牙通知
BluetoothGattDescriptor descriptor = gattCharacteristic
.getDescriptor(BtConstants.INSTANCE.getUUID_DESCRIPTOR());
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!getBluetoothGatt().writeDescriptor(descriptor)) {
callDataAvailableListenerError(BtConstants.EXCEPTION.NOTIFY_OPEN_EXCEPTION);
}
} else {
Log.i(TAG, "Disable Notification");
getBluetoothGatt().setCharacteristicNotification(gattCharacteristic, false);
BluetoothGattDescriptor descriptor = gattCharacteristic
.getDescriptor(BtConstants.INSTANCE.getUUID_DESCRIPTOR());
descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
if (!getBluetoothGatt().writeDescriptor(descriptor)) {
callDataAvailableListenerError(BtConstants.EXCEPTION.NOTIFY_CLOSE_EXCEPTION);
}
}
}
}));
}
}

@ -0,0 +1,157 @@
package com.common.bluetooth.service;
import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.common.bluetooth.callback.BtScanCallBack;
import com.common.bluetooth.interfaces.IBluetoothSearch;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
*
*
* @author wangym
* @since 2021-10-19
*/
public class BluetoothLeSearcher implements IBluetoothSearch {
private static final String TAG = "BluetoothLeSearcher";
private final BluetoothAdapter mBluetoothAdapter;
private final Handler mHandler;
private final Handler mAlertHandler;
private BtScanCallBack mScanCallback;
private final Context mContext;
private AtomicBoolean mScanning = new AtomicBoolean(false);
BluetoothLeSearcher(Context context, BluetoothAdapter adapter, Handler worker) {
mContext = context;
mBluetoothAdapter = adapter;
mHandler = worker;
HandlerThread thread = new HandlerThread("bluetooth searcher handler");
thread.start();
mAlertHandler = new Handler(thread.getLooper());
}
private BtScanCallBack wrapCallBack(final BtScanCallBack callBack) {
return new BtScanCallBack() {
@Override
public void onComplete() {
runOn(callBack::onComplete);
}
@Override
public void onError(String msg) {
runOn(() -> callBack.onError(msg));
}
@Override
public void onScanResult(int callbackType, ScanResult result) {
runOn(() -> callBack.onResult(result));
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
};
}
/**
*
*/
protected void stopLeScan() {
if (mScanning.get()) {
mScanning.set(false);
mScanCallback.onComplete();
mAlertHandler.removeCallbacksAndMessages(null);
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
mScanCallback = null;
}
}
/**
* BLE
*/
public void stopScan() {
runOn(this::stopScan);
}
private void runOn(Runnable runnable) {
mHandler.post(runnable);
}
@Override
public void startScan(long delayTime, @Nullable BtScanCallBack callBack) {
runOn(() -> {
int permissionCheck1 = ContextCompat.checkSelfPermission(mContext,
Manifest.permission.ACCESS_COARSE_LOCATION);
int permissionCheck2 = ContextCompat.checkSelfPermission(mContext,
Manifest.permission.ACCESS_FINE_LOCATION);
if ((permissionCheck1 | permissionCheck2) != PackageManager.PERMISSION_GRANTED
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String err = "Cannot have location permission";
Log.e(TAG, err);
callBack.onError(err);
return;
}
if (mScanning.get()) {
stopLeScan();
}
mScanCallback = wrapCallBack(callBack);
// Stops scanning after a pre-defined scan period.
// 预先定义停止蓝牙扫描的时间(因为蓝牙扫描需要消耗较多的电量)
mAlertHandler.removeCallbacksAndMessages(null);
mAlertHandler.postDelayed(new Runnable() {
@Override
public void run() {
stopLeScan();
}
}, delayTime);
mScanning.set(true);
// 定义一个回调接口供扫描结束处理
// 指定扫描特定的支持service的蓝牙设备
// call startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
// 可以使用rssi计算蓝牙设备的距离
// 计算公式:
// d = 10^((abs(RSSI) - A) / (10 * n))
// 其中:
// d - 计算所得距离
// RSSI - 接收信号强度(负值)
// A - 射端和接收端相隔1米时的信号强度
// n - 环境衰减因子
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
});
}
@Override
public boolean isScanning() {
return mScanning.get();
}
}

@ -0,0 +1,78 @@
package com.common.bluetooth.view;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.common.bluetooth.databinding.RvBtDeviceItemBinding;
import java.util.ArrayList;
/**
*
*
* @author wangym
* @since 2021-11-2
*/
public class BtDeviceListAdapter extends RecyclerView.Adapter<BtDeviceListAdapter.BtViewHolder> {
private static final String TAG = "BtDeviceListAdapter";
private Context context;
private ArrayList<BluetoothDevice> bluetoothDevices;
private RvBtDeviceItemBinding mBinding;
private OnDeviceClickListener onDeviceClickListener;
public BtDeviceListAdapter(Context context, ArrayList<BluetoothDevice> bluetoothDevices) {
this.context = context;
this.bluetoothDevices = bluetoothDevices;
}
@NonNull
@Override
public BtViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
mBinding = RvBtDeviceItemBinding.inflate(LayoutInflater.from(context));
return new BtViewHolder(mBinding);
}
@Override
public void onBindViewHolder(@NonNull BtViewHolder holder, int position) {
final int currentIndex = position;
BluetoothDevice device = bluetoothDevices.get(position);
holder.binding.btDeviceNameTv.setText("Device Name:" + device.getName());
holder.binding.btDeviceMacTv.setText("Device MAC:" + device.getAddress());
holder.binding.btDeviceItemRl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onDeviceClickListener != null) {
onDeviceClickListener.onClick(currentIndex);
}
}
});
}
@Override
public int getItemCount() {
return bluetoothDevices == null ? 0 : bluetoothDevices.size();
}
protected static class BtViewHolder extends RecyclerView.ViewHolder {
public RvBtDeviceItemBinding binding;
public BtViewHolder(@NonNull RvBtDeviceItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
public void setOnDeviceClickListener(OnDeviceClickListener onDeviceClickListener) {
this.onDeviceClickListener = onDeviceClickListener;
}
public interface OnDeviceClickListener {
void onClick(int index);
}
}

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.common.bluetooth.BtDemoActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/ble_rv"
android:layout_width="match_parent"
android:layout_height="200dp" />
<Button
android:id="@+id/search_ble"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="搜索蓝牙" />
<Button
android:id="@+id/build_ble_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="建立ble服务" />
<EditText
android:id="@+id/msg_et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp" />
<Button
android:id="@+id/send_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:text="发送数据" />
<TextView
android:id="@+id/receive_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:text="收到的消息:" />
<TextView
android:id="@+id/receive_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/bt_device_item_rl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/bt_device_name_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp" />
<TextView
android:id="@+id/bt_device_mac_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/bt_device_name_tv"
android:layout_marginStart="24dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="24dp" />
</RelativeLayout>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="label_color">#ffffffff</color>
<color name="label_color_hl0">#ffffffff</color>
<color name="balloon_color">#ff000000</color>
<color name="candidate_color">#ff000000</color>
<color name="active_candidate_color">#ff000000</color>
<color name="recommended_candidate_color">#ffe35900</color>
<color name="footnote_color">#ff343233</color>
<color name="composing_color">#ff000000</color>
<color name="composing_color_hl">#ffffffff</color>
<color name="composing_color_idle">#ff777777</color>
</resources>

@ -1,3 +1,4 @@
rootProject.name = "CommonLibTest" rootProject.name = "CommonLibTest"
include ':app' include ':app'
include ':commonLib' include ':commonLib'
include ':commonbt'

Loading…
Cancel
Save