From aaf76a2ab793e88ed490bcaca19a36aba6292a32 Mon Sep 17 00:00:00 2001 From: yimiao Date: Tue, 30 Nov 2021 15:07:44 +0800 Subject: [PATCH] =?UTF-8?q?[desc]:=E6=B7=BB=E5=8A=A0=E8=93=9D=E7=89=99?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=9F=BA=E7=A1=80=E5=BA=93=20[author]:wangyi?= =?UTF-8?q?miao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 4 +- build.gradle | 2 +- commonLib/build.gradle | 4 +- commonLibConfig.gradle | 6 +- commonbt/.gitignore | 1 + commonbt/build.gradle | 42 ++ commonbt/consumer-rules.pro | 0 commonbt/proguard-rules.pro | 21 + commonbt/src/main/AndroidManifest.xml | 39 ++ .../java/com/common/bluetooth/BtConstants.kt | 99 ++++ .../com/common/bluetooth/BtDemoActivity.java | 191 +++++++ .../java/com/common/bluetooth/BtManager.kt | 320 +++++++++++ .../main/java/com/common/bluetooth/BtUtils.kt | 354 ++++++++++++ .../adapter/BluetoothClientBLEAdapter.java | 270 +++++++++ .../common/bluetooth/bean/AssociateEvent.kt | 22 + .../common/bluetooth/bean/AssociateItem.kt | 22 + .../bluetooth/bean/AssociateSourceEvent.kt | 18 + .../com/common/bluetooth/bean/BaseEvent.kt | 19 + .../com/common/bluetooth/bean/CommonMsg.kt | 21 + .../common/bluetooth/bean/InputSourceEvent.kt | 18 + .../common/bluetooth/bean/KeyboardEvent.kt | 19 + .../bluetooth/callback/BLEClientListener.kt | 19 + .../callback/BaseResultCallback.java | 24 + .../callback/BleMsgReceiverListener.kt | 13 + .../bluetooth/callback/BtConnectListener.kt | 20 + .../bluetooth/callback/BtScanCallBack.kt | 29 + .../exception/BluetoothException.java | 13 + .../exception/BluetoothNotOpenException.java | 13 + .../exception/BluetoothReadException.java | 13 + .../BluetoothSearchConflictException.java | 13 + .../exception/BluetoothWriteException.java | 20 + .../bluetooth/interfaces/IBluetoothClient.kt | 120 ++++ .../bluetooth/interfaces/IBluetoothConnect.kt | 10 + .../bluetooth/interfaces/IBluetoothSearch.kt | 31 ++ .../bluetooth/service/BLEClientService.java | 320 +++++++++++ .../bluetooth/service/BLEReceiveService.java | 282 ++++++++++ .../service/BluetoothClassicSearcher.java | 45 ++ .../service/BluetoothConnectService.java | 519 ++++++++++++++++++ .../bluetooth/service/BluetoothLeClient.java | 172 ++++++ .../service/BluetoothLeConnector.java | 516 +++++++++++++++++ .../service/BluetoothLeSearcher.java | 157 ++++++ .../bluetooth/view/BtDeviceListAdapter.java | 78 +++ .../src/main/res/layout/activity_main.xml | 69 +++ .../src/main/res/layout/rv_bt_device_item.xml | 23 + commonbt/src/main/res/values/colors.xml | 21 + settings.gradle | 1 + 46 files changed, 4026 insertions(+), 7 deletions(-) create mode 100644 commonbt/.gitignore create mode 100644 commonbt/build.gradle create mode 100644 commonbt/consumer-rules.pro create mode 100644 commonbt/proguard-rules.pro create mode 100644 commonbt/src/main/AndroidManifest.xml create mode 100644 commonbt/src/main/java/com/common/bluetooth/BtConstants.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/BtDemoActivity.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/BtManager.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/BtUtils.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/adapter/BluetoothClientBLEAdapter.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/AssociateEvent.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/AssociateItem.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/AssociateSourceEvent.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/BaseEvent.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/CommonMsg.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/InputSourceEvent.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/bean/KeyboardEvent.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/callback/BLEClientListener.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/callback/BaseResultCallback.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/callback/BleMsgReceiverListener.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/callback/BtConnectListener.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/callback/BtScanCallBack.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/exception/BluetoothException.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/exception/BluetoothNotOpenException.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/exception/BluetoothReadException.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/exception/BluetoothSearchConflictException.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/exception/BluetoothWriteException.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothClient.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothConnect.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/interfaces/IBluetoothSearch.kt create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BLEClientService.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BLEReceiveService.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BluetoothClassicSearcher.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BluetoothConnectService.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeClient.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeConnector.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/service/BluetoothLeSearcher.java create mode 100644 commonbt/src/main/java/com/common/bluetooth/view/BtDeviceListAdapter.java create mode 100644 commonbt/src/main/res/layout/activity_main.xml create mode 100644 commonbt/src/main/res/layout/rv_bt_device_item.xml create mode 100644 commonbt/src/main/res/values/colors.xml 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 @@ + + + + + + + + + +