diff --git a/app/build.gradle b/app/build.gradle
index 9022e98..e3569ab 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -52,8 +52,8 @@ android {
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation project(path: ':commonLib')
diff --git a/build.gradle b/build.gradle
index f65671b..db66d8a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
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"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/commonLib/build.gradle b/commonLib/build.gradle
index c90e11a..c5c306a 100644
--- a/commonLib/build.gradle
+++ b/commonLib/build.gradle
@@ -43,8 +43,8 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation 'androidx.appcompat:appcompat:1.2.0'
- implementation 'com.google.android.material:material:1.3.0'
+ implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
diff --git a/commonLibConfig.gradle b/commonLibConfig.gradle
index 3ca5949..bc75804 100644
--- a/commonLibConfig.gradle
+++ b/commonLibConfig.gradle
@@ -36,7 +36,8 @@ ext {
glide : "4.12.0",
photo_view : "2.3.0",
luban : "1.1.8",
- kotlin_android : "1.4.1"
+ kotlin_android : "1.4.1",
+ gson : "2.8.6",
]
dependencies = [
@@ -60,6 +61,7 @@ ext {
gilde_integration : "com.github.bumptech.glide:okhttp3-integration:${versions.glide}",
annotationProcessor : "com.github.bumptech.glide:compiler:${versions.glide}",
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}",
]
}
\ No newline at end of file
diff --git a/commonbt/.gitignore b/commonbt/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/commonbt/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/commonbt/build.gradle b/commonbt/build.gradle
new file mode 100644
index 0000000..37559f3
--- /dev/null
+++ b/commonbt/build.gradle
@@ -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
+}
\ No newline at end of file
diff --git a/commonbt/consumer-rules.pro b/commonbt/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/commonbt/proguard-rules.pro b/commonbt/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/commonbt/proguard-rules.pro
@@ -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
\ No newline at end of file
diff --git a/commonbt/src/main/AndroidManifest.xml b/commonbt/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7b80fd7
--- /dev/null
+++ b/commonbt/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/BtConstants.kt b/commonbt/src/main/java/com/common/bluetooth/BtConstants.kt
new file mode 100644
index 0000000..ea514ae
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/BtConstants.kt
@@ -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())
+ }
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/BtDemoActivity.java b/commonbt/src/main/java/com/common/bluetooth/BtDemoActivity.java
new file mode 100644
index 0000000..6310fa0
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/BtDemoActivity.java
@@ -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 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 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);
+ }
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/BtManager.kt b/commonbt/src/main/java/com/common/bluetooth/BtManager.kt
new file mode 100644
index 0000000..11af4b9
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/BtManager.kt
@@ -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 {
+ 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? {
+ 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/BtUtils.kt b/commonbt/src/main/java/com/common/bluetooth/BtUtils.kt
new file mode 100644
index 0000000..e17ee9f
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/BtUtils.kt
@@ -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位 事件类型 1:click 2:drag 3:switch
+ * 第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 {
+ // 默认是 完整包 键盘事件 交互事件
+ 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
+ }
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/adapter/BluetoothClientBLEAdapter.java b/commonbt/src/main/java/com/common/bluetooth/adapter/BluetoothClientBLEAdapter.java
new file mode 100644
index 0000000..a65eecf
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/adapter/BluetoothClientBLEAdapter.java
@@ -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 notifyCallback = null;
+
+ public BluetoothClientBLEAdapter(BluetoothLeClient client) {
+ mClient = client;
+
+ HandlerThread workThread = new HandlerThread("bluetooth ble worker");
+ workThread.start();
+ }
+
+ @Override
+ public Observable search(final int millis, final boolean cancel) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(@NonNull final ObservableEmitter 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 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 connect(final String mac) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(@NonNull final ObservableEmitter 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 write(@NonNull final String mac, @NonNull final UUID service,
+ @NonNull final UUID characteristic,
+ @NonNull final byte[] values) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(@NonNull final ObservableEmitter 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 notifyCallback) {
+ this.notifyCallback = notifyCallback;
+ }
+
+ @Override
+ public Observable registerNotify(@NonNull final String mac, @NonNull final UUID service,
+ @NonNull final UUID characteristic,
+ final BaseResultCallback callback) {
+ return Observable.create(new ObservableOnSubscribe() {
+ @Override
+ public void subscribe(@NonNull final ObservableEmitter 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 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 getBondedDevices() {
+ return mClient.getBondedDevices();
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/AssociateEvent.kt b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateEvent.kt
new file mode 100644
index 0000000..41695be
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateEvent.kt
@@ -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 = ArrayList()
+) : Parcelable
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/AssociateItem.kt b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateItem.kt
new file mode 100644
index 0000000..afe948e
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateItem.kt
@@ -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
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/AssociateSourceEvent.kt b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateSourceEvent.kt
new file mode 100644
index 0000000..29a22dd
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/AssociateSourceEvent.kt
@@ -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
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/BaseEvent.kt b/commonbt/src/main/java/com/common/bluetooth/bean/BaseEvent.kt
new file mode 100644
index 0000000..1f401e9
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/BaseEvent.kt
@@ -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
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/CommonMsg.kt b/commonbt/src/main/java/com/common/bluetooth/bean/CommonMsg.kt
new file mode 100644
index 0000000..0482d09
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/CommonMsg.kt
@@ -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
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/InputSourceEvent.kt b/commonbt/src/main/java/com/common/bluetooth/bean/InputSourceEvent.kt
new file mode 100644
index 0000000..5d02b3a
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/InputSourceEvent.kt
@@ -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
diff --git a/commonbt/src/main/java/com/common/bluetooth/bean/KeyboardEvent.kt b/commonbt/src/main/java/com/common/bluetooth/bean/KeyboardEvent.kt
new file mode 100644
index 0000000..bc34690
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/bean/KeyboardEvent.kt
@@ -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 = ArrayList()
+) : Parcelable
diff --git a/commonbt/src/main/java/com/common/bluetooth/callback/BLEClientListener.kt b/commonbt/src/main/java/com/common/bluetooth/callback/BLEClientListener.kt
new file mode 100644
index 0000000..0d383a6
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/callback/BLEClientListener.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/callback/BaseResultCallback.java b/commonbt/src/main/java/com/common/bluetooth/callback/BaseResultCallback.java
new file mode 100644
index 0000000..f1fef36
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/callback/BaseResultCallback.java
@@ -0,0 +1,24 @@
+package com.common.bluetooth.callback;
+
+/**
+ * 用于数据回传的基本回调接口
+ *
+ * @author wangym
+ * @since 2021-10-19
+ */
+public interface BaseResultCallback {
+
+ /**
+ * 成功拿到数据
+ *
+ * @param data 回传的数据
+ */
+ void onSuccess(T data);
+
+ /**
+ * 操作失败
+ *
+ * @param msg 失败的返回的异常信息
+ */
+ void onFail(String msg);
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/callback/BleMsgReceiverListener.kt b/commonbt/src/main/java/com/common/bluetooth/callback/BleMsgReceiverListener.kt
new file mode 100644
index 0000000..5986236
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/callback/BleMsgReceiverListener.kt
@@ -0,0 +1,13 @@
+package com.common.bluetooth.callback
+
+/**
+ * BLE数据接收监听
+ */
+interface BleMsgReceiverListener {
+ /**
+ * 接收的消息
+ *
+ * @param msg 传输的数据
+ */
+ fun onMsgReceive(msg: ByteArray)
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/callback/BtConnectListener.kt b/commonbt/src/main/java/com/common/bluetooth/callback/BtConnectListener.kt
new file mode 100644
index 0000000..db67372
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/callback/BtConnectListener.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/callback/BtScanCallBack.kt b/commonbt/src/main/java/com/common/bluetooth/callback/BtScanCallBack.kt
new file mode 100644
index 0000000..1ea18e9
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/callback/BtScanCallBack.kt
@@ -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?) {}
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothException.java b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothException.java
new file mode 100644
index 0000000..c5162ff
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothException.java
@@ -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);
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothNotOpenException.java b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothNotOpenException.java
new file mode 100644
index 0000000..4afa49e
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothNotOpenException.java
@@ -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);
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothReadException.java b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothReadException.java
new file mode 100644
index 0000000..6820438
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothReadException.java
@@ -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);
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothSearchConflictException.java b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothSearchConflictException.java
new file mode 100644
index 0000000..5a36b8c
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothSearchConflictException.java
@@ -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);
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothWriteException.java b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothWriteException.java
new file mode 100644
index 0000000..c43b04c
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/exception/BluetoothWriteException.java
@@ -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;
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothClient.kt b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothClient.kt
new file mode 100644
index 0000000..11c01cb
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothClient.kt
@@ -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
+
+ /**
+ * 停止扫描
+ */
+ fun stopSearch()
+
+ /**
+ * 连接一台蓝牙设备. 连接的蓝牙设备有最大限制,
+ * 如果超出这一个数量,即使连接上了蓝牙设备也扫描不到该设备的服务通道
+ *
+ * @param mac 需要连接蓝牙设备的地址
+ * @return 成功,返回连接设备的地址
+ */
+ fun connect(mac: String?): Observable
+
+ /**
+ * 断开蓝牙连接, 释放蓝牙连接占用的蓝牙服务
+ *
+ * @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?
+
+ /**
+ * 向蓝牙设备注册一个通道值改变的监听器,
+ * 每一个设备的每一个通道只允许同时存在一个监听器。
+ *
+ * @param mac 需要监听的 mac 地址
+ * @param service 需要监听的设备的服务地址
+ * @param characteristic 需要监听设备的 characteristic
+ * @param callback 需要注册的监听器
+ * @return 成功或失败返回
+ */
+ fun registerNotify(
+ mac: String, service: UUID, characteristic: UUID,
+ callback: BaseResultCallback?
+ ): Observable
+
+ /**
+ * 解除在对应设备对应通道注册了的监听器
+ *
+ * @param mac 需要监听的 mac 地址
+ * @param service 需要监听的设备的服务地址
+ * @param characteristic 需要监听设备的 characteristic
+ */
+ fun unRegisterNotify(mac: String, service: UUID, characteristic: UUID): Observable?
+
+ /**
+ * 清空对应 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?
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothConnect.kt b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothConnect.kt
new file mode 100644
index 0000000..7b117c4
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothConnect.kt
@@ -0,0 +1,10 @@
+package com.common.bluetooth.interfaces
+
+/**
+ * 蓝牙连接接口
+ *
+ * @author wangym
+ * @since 2021-11-9
+ */
+interface IBluetoothConnect {
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothSearch.kt b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothSearch.kt
new file mode 100644
index 0000000..846972c
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothSearch.kt
@@ -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
+}
\ No newline at end of file
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BLEClientService.java b/commonbt/src/main/java/com/common/bluetooth/service/BLEClientService.java
new file mode 100644
index 0000000..afdc4db
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BLEClientService.java
@@ -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 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 notifyCallback = new BaseResultCallback() {
+ @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[] 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() {
+ @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[] 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 observable = mBtClient.write(connectMac, BtConstants.INSTANCE.getUUID_SERVICE(),
+ BtConstants.INSTANCE.getUUID_CHARACTERISTIC_WRITE(),
+ temp);
+ observables[index] = observable;
+ }
+
+ Observable.concatArray(observables).subscribe(new Observer() {
+ @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;
+ }
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BLEReceiveService.java b/commonbt/src/main/java/com/common/bluetooth/service/BLEReceiveService.java
new file mode 100644
index 0000000..fc62fd0
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BLEReceiveService.java
@@ -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("onResponseToClient:device name = %s, address = %s", device.getName(),
+ device.getAddress()));
+ Log.d(TAG, String.format("onResponseToClient:requestId = %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);
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BluetoothClassicSearcher.java b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothClassicSearcher.java
new file mode 100644
index 0000000..2688e3e
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothClassicSearcher.java
@@ -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();
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BluetoothConnectService.java b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothConnectService.java
new file mode 100644
index 0000000..4631051
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothConnectService.java
@@ -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);
+ }
+ }
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeClient.java b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeClient.java
new file mode 100644
index 0000000..cee6db1
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeClient.java
@@ -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 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 getBondedDevices() {
+ return mBluetoothAdapter.getBondedDevices();
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeConnector.java b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeConnector.java
new file mode 100644
index 0000000..8b32339
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeConnector.java
@@ -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 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() {
+ @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() {
+ @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);
+ }
+ }
+ }
+ }));
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeSearcher.java b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeSearcher.java
new file mode 100644
index 0000000..6819967
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeSearcher.java
@@ -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 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();
+ }
+}
diff --git a/commonbt/src/main/java/com/common/bluetooth/view/BtDeviceListAdapter.java b/commonbt/src/main/java/com/common/bluetooth/view/BtDeviceListAdapter.java
new file mode 100644
index 0000000..1fb62df
--- /dev/null
+++ b/commonbt/src/main/java/com/common/bluetooth/view/BtDeviceListAdapter.java
@@ -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 {
+ private static final String TAG = "BtDeviceListAdapter";
+ private Context context;
+ private ArrayList bluetoothDevices;
+ private RvBtDeviceItemBinding mBinding;
+ private OnDeviceClickListener onDeviceClickListener;
+
+ public BtDeviceListAdapter(Context context, ArrayList 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);
+ }
+}
diff --git a/commonbt/src/main/res/layout/activity_main.xml b/commonbt/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..90ec27c
--- /dev/null
+++ b/commonbt/src/main/res/layout/activity_main.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commonbt/src/main/res/layout/rv_bt_device_item.xml b/commonbt/src/main/res/layout/rv_bt_device_item.xml
new file mode 100644
index 0000000..3080776
--- /dev/null
+++ b/commonbt/src/main/res/layout/rv_bt_device_item.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commonbt/src/main/res/values/colors.xml b/commonbt/src/main/res/values/colors.xml
new file mode 100644
index 0000000..df7101c
--- /dev/null
+++ b/commonbt/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+ #ffffffff
+ #ffffffff
+ #ff000000
+ #ff000000
+ #ff000000
+ #ffe35900
+ #ff343233
+ #ff000000
+ #ffffffff
+ #ff777777
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index d5d5c7b..7c3de11 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,4 @@
rootProject.name = "CommonLibTest"
include ':app'
include ':commonLib'
+include ':commonbt'