├── .gitignore ├── .idea ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zhengsr │ │ └── bluetoothdemo │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zhengsr │ │ │ └── bluetoothdemo │ │ │ ├── MainActivity.kt │ │ │ ├── bean │ │ │ └── BlueBean.kt │ │ │ ├── bluetooth │ │ │ ├── a2dp │ │ │ │ └── A2dpActivity.kt │ │ │ ├── ble │ │ │ │ ├── BleBlueImpl.kt │ │ │ │ ├── BleClientActivity.kt │ │ │ │ └── BleServerActivity.kt │ │ │ └── bt │ │ │ │ ├── BtBlueImpl.kt │ │ │ │ ├── BtClientActivity.kt │ │ │ │ ├── BtServerActivity.kt │ │ │ │ └── HandleSocket.kt │ │ │ └── utils │ │ │ ├── CloseUtils.kt │ │ │ └── Tool.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_a2dp.xml │ │ ├── activity_bl_client.xml │ │ ├── activity_bl_server.xml │ │ ├── activity_ble_client.xml │ │ ├── activity_ble_server.xml │ │ ├── activity_main.xml │ │ ├── recy_ble_item_layout.xml │ │ └── recy_blue_item_layout.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zhengsr │ └── bluetoothdemo │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | BluetoothDemo -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BluetoothDemo 2 | 蓝牙开发Demo,传统蓝牙聊天室,A2DP蓝牙音响,BLE低功耗蓝牙等 3 | 4 | ## 已完成 5 | 6 | 1. [Android 蓝牙开发(一) -- 传统蓝牙聊天室](https://blog.csdn.net/u011418943/article/details/107818438) 7 | 2. [Android 蓝牙开发(二) --手机与蓝牙音箱配对,并播放音频](https://blog.csdn.net/u011418943/article/details/107849830) 8 | 3. [Android 蓝牙开发(三) -- 低功耗蓝牙开发](https://blog.csdn.net/u011418943/article/details/108401011) 9 | 10 | 更多音视频,参考:[Android 音视频入门/进阶教程](https://blog.csdn.net/u011418943/article/details/128478498?spm=1001.2014.3001.5502) 11 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | 8 | defaultConfig { 9 | applicationId "com.zhengsr.bluetoothdemo" 10 | minSdkVersion 19 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: "libs", include: ["*.jar"]) 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 29 | implementation 'androidx.core:core-ktx:1.3.1' 30 | implementation 'androidx.appcompat:appcompat:1.1.0' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 35 | implementation 'com.github.LillteZheng:ZPermission:v1.0' 36 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' 37 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7' 38 | 39 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 40 | implementation 'androidx.cardview:cardview:1.0.0' 41 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4' 42 | 43 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/zhengsr/bluetoothdemo/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.zhengsr.bluetoothdemo", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.bluetooth.BluetoothAdapter 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.location.LocationManager 9 | import android.os.Build 10 | import android.os.Bundle 11 | import android.view.View 12 | import android.widget.Toast 13 | import androidx.appcompat.app.AppCompatActivity 14 | import com.zhengsr.bluetoothdemo.bluetooth.a2dp.A2dpActivity 15 | import com.zhengsr.bluetoothdemo.bluetooth.ble.BleClientActivity 16 | import com.zhengsr.bluetoothdemo.bluetooth.ble.BleServerActivity 17 | import com.zhengsr.bluetoothdemo.bluetooth.bt.BtClientActivity 18 | import com.zhengsr.bluetoothdemo.bluetooth.bt.BtServerActivity 19 | import com.zhengsr.zplib.ZPermission 20 | 21 | const val TAG = "MainActivity" 22 | 23 | class MainActivity : AppCompatActivity() { 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | setContentView(R.layout.activity_main) 27 | 28 | requestPermission() 29 | 30 | val bluetooth = BluetoothAdapter.getDefaultAdapter() 31 | if (bluetooth == null) { 32 | Toast.makeText(this, "您的设备未找到蓝牙驱动!!", Toast.LENGTH_SHORT).show() 33 | finish() 34 | }else { 35 | if (!bluetooth.isEnabled) { 36 | startActivityForResult(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE),1) 37 | } 38 | } 39 | 40 | 41 | } 42 | 43 | 44 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 45 | super.onActivityResult(requestCode, resultCode, data) 46 | if (requestCode == 1){ 47 | if (resultCode == Activity.RESULT_CANCELED){ 48 | Toast.makeText(this, "请您不要拒绝开启蓝牙,否则应用无法运行", Toast.LENGTH_SHORT).show() 49 | finish() 50 | } 51 | } 52 | } 53 | 54 | private fun requestPermission(){ 55 | 56 | ZPermission.with(this) 57 | .permissions( 58 | Manifest.permission.ACCESS_FINE_LOCATION, 59 | Manifest.permission.BLUETOOTH_ADMIN 60 | ).request { isAllGranted, deniedLists -> 61 | if (!isAllGranted){ 62 | Toast.makeText(this, "需要开启权限才能运行应用", Toast.LENGTH_SHORT).show() 63 | finish() 64 | } 65 | } 66 | 67 | 68 | //在 Android 10 还需要开启 gps 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 70 | val lm: LocationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager 71 | if (!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)){ 72 | Toast.makeText(this@MainActivity, "请您先开启gps,否则蓝牙不可用", Toast.LENGTH_SHORT).show() 73 | } 74 | } 75 | 76 | } 77 | 78 | fun client(view: View) {startActivity(Intent(this, 79 | BtClientActivity::class.java))} 80 | fun server(view: View) {startActivity(Intent(this, 81 | BtServerActivity::class.java))} 82 | 83 | fun a2dpclient(view: View) {startActivity(Intent(this, 84 | A2dpActivity::class.java))} 85 | 86 | fun bleServer(view: View) {startActivity(Intent(this,BleServerActivity::class.java))} 87 | fun bleClient(view: View) {startActivity(Intent(this,BleClientActivity::class.java))} 88 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bean/BlueBean.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bean 2 | 3 | import android.bluetooth.BluetoothDevice 4 | 5 | data class BlueBean(val bluetoothDevice: BluetoothDevice, var status: Boolean) {} -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/a2dp/A2dpActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.a2dp 2 | 3 | import android.bluetooth.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.graphics.Color 7 | import android.os.Bundle 8 | import android.util.Log 9 | import android.view.View 10 | import android.widget.TextView 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import androidx.recyclerview.widget.RecyclerView 15 | import com.chad.library.adapter.base.BaseQuickAdapter 16 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 17 | import com.zhengsr.bluetoothdemo.R 18 | import com.zhengsr.bluetoothdemo.bluetooth.bt.BlueBroadcastListener 19 | import com.zhengsr.bluetoothdemo.bluetooth.bt.BtBlueImpl 20 | import com.zhengsr.bluetoothdemo.utils.close 21 | 22 | class A2dpActivity : AppCompatActivity() { 23 | 24 | companion object { 25 | private val TAG = javaClass.simpleName 26 | } 27 | 28 | /** 29 | * UI 30 | */ 31 | private var itemStateTv: TextView? = null 32 | private var logTv: TextView? = null 33 | 34 | /** 35 | * logic 36 | */ 37 | private var blueBeans: MutableList = mutableListOf() 38 | private lateinit var blueAdapter: BlueAdapter 39 | private lateinit var bluetooth: BluetoothAdapter 40 | private var bluetoothA2dp: BluetoothA2dp? = null 41 | private var connectThread: ConnectThread? = null 42 | private val stringBuilder = StringBuilder() 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | setContentView(R.layout.activity_a2dp) 47 | 48 | logTv = findViewById(R.id.log_tv) 49 | 50 | bluetooth = BluetoothAdapter.getDefaultAdapter() 51 | initRecyclerView() 52 | 53 | val broadcast = listOf( 54 | BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED, 55 | BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED 56 | ) 57 | 58 | 59 | BtBlueImpl.init(this) 60 | .registerBroadcast(broadcast, blueStateListener) 61 | .foundDevices { bean -> 62 | if (bean !in blueBeans && bean.name != null) { 63 | blueBeans.add(bean) 64 | blueAdapter.notifyItemInserted(blueBeans.size) 65 | } 66 | } 67 | 68 | 69 | bluetooth.getProfileProxy(this, object : BluetoothProfile.ServiceListener { 70 | override fun onServiceDisconnected(profile: Int) { 71 | 72 | if (profile == BluetoothProfile.A2DP) { 73 | bluetoothA2dp = null 74 | } 75 | } 76 | 77 | override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) { 78 | if (profile == BluetoothProfile.A2DP) { 79 | bluetoothA2dp = proxy as BluetoothA2dp 80 | 81 | 82 | } 83 | } 84 | 85 | }, BluetoothProfile.A2DP) 86 | } 87 | 88 | /** 89 | * 初始化 recyclerview 90 | */ 91 | private fun initRecyclerView() { 92 | val recyclerView: RecyclerView = findViewById(R.id.recycler) 93 | val manager = LinearLayoutManager(this) 94 | 95 | recyclerView.layoutManager = manager 96 | blueAdapter = BlueAdapter( 97 | blueBeans, 98 | R.layout.recy_blue_item_layout 99 | ) 100 | blueAdapter.animationEnable = true 101 | recyclerView.adapter = blueAdapter 102 | 103 | blueAdapter.setOnItemClickListener { baseQuickAdapter: BaseQuickAdapter<*, *>, view: View, i: Int -> 104 | val dev: BluetoothDevice = blueBeans[i] 105 | Toast.makeText(this, "开始连接...", Toast.LENGTH_SHORT).show() 106 | itemStateTv = view.findViewById(R.id.blue_item_status_tv) 107 | 108 | connectThread = ConnectThread(dev, object : 109 | ConnectListener { 110 | override fun onStart() { 111 | Log.d(TAG, "zsr onStart: ") 112 | } 113 | 114 | override fun onConnected() { 115 | Log.d(TAG, "zsr onConnected: ") 116 | } 117 | 118 | override fun onFail(errorMsg: String) { 119 | Log.d(TAG, "zsr onFail: $errorMsg") 120 | } 121 | 122 | }) 123 | connectThread?.start() 124 | 125 | } 126 | } 127 | 128 | inner class ConnectThread( 129 | private val device: BluetoothDevice, 130 | private val listener: ConnectListener 131 | ) : Thread() { 132 | private var socket: BluetoothSocket? = null 133 | 134 | override fun run() { 135 | super.run() 136 | listener.onStart() 137 | bluetooth.cancelDiscovery() 138 | while (true) { 139 | try { 140 | //先绑定 141 | if (device.bondState != BluetoothDevice.BOND_BONDED) { 142 | val createSocket = 143 | BluetoothDevice::class.java.getMethod( 144 | "createRfcommSocket", 145 | Int::class.java 146 | ) 147 | createSocket.isAccessible = true 148 | //找一个通道去连接即可,channel 1~30 149 | socket = createSocket.invoke(device, 1) as BluetoothSocket 150 | //阻塞等待 151 | socket?.connect() 152 | //延时,以便于去连接 153 | sleep(2000) 154 | } 155 | 156 | if (connectA2dp(device)) { 157 | listener.onConnected() 158 | break 159 | } else { 160 | listener.onFail("Blue connect fail ") 161 | } 162 | 163 | } catch (e: Exception) { 164 | listener.onFail(e.toString()) 165 | return 166 | } 167 | } 168 | } 169 | 170 | fun cancel() { 171 | close(socket) 172 | //取消绑定 173 | try { 174 | //通过反射获取BluetoothA2dp中connect方法(hide的),断开连接。 175 | val connectMethod = BluetoothA2dp::class.java.getMethod( 176 | "disconnect", 177 | BluetoothDevice::class.java 178 | ) 179 | connectMethod.invoke(bluetoothA2dp, device) 180 | } catch (e: Exception) { 181 | e.printStackTrace() 182 | } 183 | } 184 | } 185 | 186 | 187 | private fun connectA2dp(device: BluetoothDevice):Boolean{ 188 | //连接 a2dp 189 | val connect = 190 | BluetoothA2dp::class.java.getMethod("connect", BluetoothDevice::class.java) 191 | connect.isAccessible = true 192 | return connect.invoke(bluetoothA2dp, device) as Boolean 193 | } 194 | 195 | /** 196 | * recyclerview 的 adapter 197 | */ 198 | class BlueAdapter(datas: MutableList, layoutResId: Int) : 199 | BaseQuickAdapter(layoutResId, datas) { 200 | override fun convert(holder: BaseViewHolder, item: BluetoothDevice) { 201 | 202 | holder.setText(R.id.blue_item_addr_tv, item.address) 203 | holder.setText(R.id.blue_item_name_tv, item.name) 204 | 205 | val statusTv = holder.getView(R.id.blue_item_status_tv) 206 | if (item.bondState == BluetoothDevice.BOND_BONDED) { 207 | statusTv.text = "(已配对)" 208 | statusTv.setTextColor(Color.parseColor("#ff009688")) 209 | } else { 210 | statusTv.text = "(未配对)" 211 | statusTv.setTextColor(Color.parseColor("#ffFF5722")) 212 | 213 | } 214 | } 215 | 216 | } 217 | 218 | 219 | val blueStateListener = object : 220 | BlueBroadcastListener { 221 | override fun invoke(context: Context?, intent: Intent?) { 222 | when (intent?.action) { 223 | BluetoothDevice.ACTION_BOND_STATE_CHANGED -> { 224 | val dev = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 225 | val state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,0) 226 | Log.d(TAG, "zsr invoke: $state && ${dev.name}") 227 | //connectA2dp(dev) 228 | } 229 | BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED -> { 230 | val state = intent.getIntExtra( 231 | BluetoothA2dp.EXTRA_STATE, 232 | BluetoothA2dp.STATE_DISCONNECTED 233 | ) 234 | if (state == BluetoothA2dp.STATE_CONNECTING) { 235 | 236 | itemStateTv?.text = "正在连接..." 237 | } else if (state == BluetoothA2dp.STATE_CONNECTED) { 238 | itemStateTv?.text = "连接成功" 239 | } 240 | 241 | } 242 | 243 | BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED -> { 244 | val state = intent.getIntExtra( 245 | BluetoothA2dp.EXTRA_STATE, 246 | BluetoothA2dp.STATE_NOT_PLAYING 247 | ) 248 | when (state) { 249 | BluetoothA2dp.STATE_PLAYING -> { 250 | itemStateTv?.text = "正在播放" 251 | } 252 | BluetoothA2dp.STATE_NOT_PLAYING -> { 253 | itemStateTv?.text = "播放停止" 254 | } 255 | } 256 | } 257 | } 258 | } 259 | 260 | } 261 | 262 | 263 | fun scan(view: View) { 264 | blueBeans.clear() 265 | blueAdapter.notifyDataSetChanged() 266 | BtBlueImpl.foundDevices { bean -> 267 | if (bean !in blueBeans && bean.name != null) { 268 | blueBeans.add(bean) 269 | blueAdapter.notifyItemInserted(blueBeans.size) 270 | } 271 | } 272 | } 273 | 274 | interface ConnectListener { 275 | fun onStart() 276 | fun onConnected() 277 | fun onFail(errorMsg: String) 278 | } 279 | 280 | override fun onDestroy() { 281 | super.onDestroy() 282 | BtBlueImpl.release() 283 | bluetooth.closeProfileProxy(BluetoothProfile.A2DP,bluetoothA2dp) 284 | connectThread?.cancel() 285 | } 286 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/ble/BleBlueImpl.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.ble 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.bluetooth.BluetoothDevice 5 | import android.bluetooth.le.ScanCallback 6 | import android.bluetooth.le.ScanResult 7 | import android.bluetooth.le.ScanSettings 8 | import android.os.Build 9 | import android.os.Handler 10 | import android.os.Looper 11 | import androidx.annotation.RequiresApi 12 | import java.util.* 13 | 14 | /** 15 | * @author by zhengshaorui 2020/9/3 17:26 16 | * describe:专门给低功耗蓝牙的 17 | */ 18 | data class BleData(val dev: BluetoothDevice, val scanRecord: String? = null) 19 | typealias BleDevListener = (BleData) -> Unit 20 | 21 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 22 | object BleBlueImpl { 23 | val UUID_SERVICE = UUID.fromString("10000000-0000-0000-0000-000000000000") 24 | val UUID_READ_NOTIFY = UUID.fromString("11000000-0000-0000-0000-000000000000") 25 | val UUID_WRITE = UUID.fromString("12000000-0000-0000-0000-000000000000") 26 | val UUID_DESCRIBE = UUID.fromString("12000000-0000-0000-0000-000000000000") 27 | val handler = Handler(Looper.getMainLooper()) 28 | private var isScanning = false 29 | private var bluetoothAdapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter() 30 | private var devCallback: BleDevListener? = null 31 | 32 | fun scanDev(callback: BleDevListener) { 33 | devCallback = callback 34 | if (isScanning) { 35 | return 36 | } 37 | 38 | //扫描设置 39 | 40 | val builder = ScanSettings.Builder() 41 | /** 42 | * 三种模式 43 | * - SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式 44 | * - SCAN_MODE_BALANCED : 平衡模式,一定频率下返回结果 45 | * - SCAN_MODE_LOW_LATENCY 高功耗模式,建议应用在前台才使用此模式 46 | */ 47 | .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)//高功耗,应用在前台 48 | 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 50 | /** 51 | * 三种回调模式 52 | * - CALLBACK_TYPE_ALL_MATCHED : 寻找符合过滤条件的广播,如果没有,则返回全部广播 53 | * - CALLBACK_TYPE_FIRST_MATCH : 仅筛选匹配第一个广播包出发结果回调的 54 | * - CALLBACK_TYPE_MATCH_LOST : 这个看英文文档吧,不满足第一个条件的时候,不好解释 55 | */ 56 | builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 57 | } 58 | 59 | //判断手机蓝牙芯片是否支持皮批处理扫描 60 | if (bluetoothAdapter.isOffloadedFilteringSupported) { 61 | builder.setReportDelay(0L) 62 | } 63 | 64 | 65 | 66 | isScanning = true 67 | //扫描是很耗电的,所以,我们不能持续扫描 68 | handler.postDelayed({ 69 | 70 | bluetoothAdapter.bluetoothLeScanner?.stopScan(scanListener) 71 | isScanning = false; 72 | }, 3000) 73 | bluetoothAdapter.bluetoothLeScanner?.startScan(null, builder.build(), scanListener) 74 | //过滤特定的 UUID 设备 75 | //bluetoothAdapter?.bluetoothLeScanner?.startScan() 76 | } 77 | 78 | private val scanListener = object : ScanCallback() { 79 | override fun onScanResult(callbackType: Int, result: ScanResult?) { 80 | super.onScanResult(callbackType, result) 81 | //不断回调,所以不建议做复杂的动作 82 | result ?: return 83 | 84 | 85 | result.device.name ?: return 86 | 87 | val bean = BleData(result.device, result.scanRecord.toString()) 88 | devCallback?.let { 89 | it(bean) 90 | } 91 | 92 | 93 | } 94 | } 95 | 96 | 97 | fun stopScan(){ 98 | bluetoothAdapter.bluetoothLeScanner?.stopScan(scanListener) 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/ble/BleClientActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.ble 2 | 3 | import android.bluetooth.* 4 | import android.content.pm.PackageManager 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import android.view.View 10 | import android.widget.EditText 11 | import android.widget.TextView 12 | import android.widget.Toast 13 | import androidx.annotation.RequiresApi 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.recyclerview.widget.LinearLayoutManager 16 | import androidx.recyclerview.widget.RecyclerView 17 | import com.chad.library.adapter.base.BaseQuickAdapter 18 | import com.chad.library.adapter.base.listener.OnItemClickListener 19 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 20 | import com.zhengsr.bluetoothdemo.R 21 | import java.util.* 22 | 23 | /** 24 | * @author zhengshaorui 25 | * 中心设备,可以扫描到多个外围设备,并从外围设备获取信息 26 | */ 27 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 28 | class BleClientActivity : AppCompatActivity(), OnItemClickListener { 29 | 30 | 31 | val handler = Handler(Looper.getMainLooper()) 32 | private var mScanning: Boolean = false; 33 | private var mBleAdapter: BlueAdapter? = null 34 | private val mData: MutableList = mutableListOf(); 35 | private var mBluetoothGatt: BluetoothGatt? = null 36 | private val mSb = StringBuilder() 37 | private lateinit var mInfoTv: TextView; 38 | private var bluetoothAdapter: BluetoothAdapter? = null; 39 | private var blueGatt: BluetoothGatt? = null 40 | private var isConnected = false 41 | private lateinit var editText: EditText 42 | companion object { 43 | private val TAG = "BleClientActivity" 44 | } 45 | 46 | override fun onCreate(savedInstanceState: Bundle?) { 47 | super.onCreate(savedInstanceState) 48 | setContentView(R.layout.activity_ble_client) 49 | mInfoTv = findViewById(R.id.info_tv) 50 | editText = findViewById(R.id.edit) 51 | initRecyclerView() 52 | 53 | //是否支持低功耗蓝牙 54 | initBluetooth() 55 | } 56 | 57 | 58 | 59 | private fun initBluetooth() { 60 | packageManager.takeIf { !it.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) } 61 | ?.let { 62 | Toast.makeText(this, "您的设备没有低功耗蓝牙驱动!", Toast.LENGTH_SHORT).show() 63 | finish() 64 | } 65 | 66 | bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() 67 | } 68 | 69 | /** 70 | * 初始化 recyclerview 71 | */ 72 | private fun initRecyclerView() { 73 | val recyclerView: RecyclerView = findViewById(R.id.recycler) 74 | val manager = LinearLayoutManager(this) 75 | recyclerView.layoutManager = manager 76 | mBleAdapter = BlueAdapter(R.layout.recy_ble_item_layout, mData) 77 | recyclerView.adapter = mBleAdapter 78 | 79 | mBleAdapter?.setOnItemClickListener(this) 80 | } 81 | 82 | class BlueAdapter(layoutId: Int, datas: MutableList) : 83 | BaseQuickAdapter(layoutId, datas) { 84 | override fun convert(holder: BaseViewHolder, item: BleData) { 85 | //没有名字不显示 86 | holder.setText(R.id.item_ble_name_tv, "名称: " + item.dev.name ?: "Null") 87 | .setText(R.id.item_ble_mac_tv, "地址: " + item.dev.address) 88 | .setText(R.id.item_ble_device_tv, item.scanRecord) 89 | } 90 | 91 | } 92 | 93 | override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 94 | //连接之前先关闭连接 95 | closeConnect() 96 | val bleData = mData[position] 97 | blueGatt = bleData.dev.connectGatt(this, false, blueGattListener) 98 | logInfo("开始与 ${bleData.dev.name} 连接.... $blueGatt") 99 | } 100 | 101 | 102 | /** 103 | * 断开连接 104 | */ 105 | private fun closeConnect() { 106 | BleBlueImpl.stopScan() 107 | blueGatt?.let { 108 | it.disconnect() 109 | it.close() 110 | } 111 | 112 | } 113 | 114 | 115 | private val blueGattListener = object : BluetoothGattCallback() { 116 | override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { 117 | super.onConnectionStateChange(gatt, status, newState) 118 | val device = gatt?.device 119 | if (newState == BluetoothProfile.STATE_CONNECTED){ 120 | isConnected = true 121 | //开始发现服务,有个小延时,最后200ms后尝试发现服务 122 | handler.postDelayed({ 123 | gatt?.discoverServices() 124 | },300) 125 | 126 | device?.let{logInfo("与 ${it.name} 连接成功!!!")} 127 | }else if (newState == BluetoothProfile.STATE_DISCONNECTED){ 128 | isConnected = false 129 | logInfo("无法与 ${device?.name} 连接: $status") 130 | closeConnect() 131 | } 132 | } 133 | 134 | override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { 135 | super.onServicesDiscovered(gatt, status) 136 | // Log.d(TAG, "zsr onServicesDiscovered: ${gatt?.device?.name}") 137 | val service = gatt?.getService(BleBlueImpl.UUID_SERVICE) 138 | mBluetoothGatt = gatt 139 | logInfo("已连接上 GATT 服务,可以通信! ") 140 | 141 | /*if (status == BluetoothGatt.GATT_SUCCESS){ 142 | gatt?.services?.forEach {service -> 143 | logInfo("service 的 uuid: ${service.uuid}") 144 | service.characteristics.forEach{ characteristic -> 145 | logInfo("characteristic 的 uuid: ${characteristic.uuid}") 146 | characteristic.descriptors.forEach { descrip -> 147 | logInfo("descrip 的 uuid: ${descrip.uuid}") 148 | } 149 | } 150 | } 151 | }*/ 152 | } 153 | 154 | override fun onCharacteristicRead( 155 | gatt: BluetoothGatt?, 156 | characteristic: BluetoothGattCharacteristic?, 157 | status: Int 158 | ) { 159 | super.onCharacteristicRead(gatt, characteristic, status) 160 | characteristic?.let { 161 | val data = String(it.value) 162 | logInfo("CharacteristicRead 数据: $data") 163 | } 164 | } 165 | 166 | override fun onCharacteristicWrite( 167 | gatt: BluetoothGatt?, 168 | characteristic: BluetoothGattCharacteristic?, 169 | status: Int 170 | ) { 171 | super.onCharacteristicWrite(gatt, characteristic, status) 172 | characteristic?.let { 173 | val data = String(it.value) 174 | logInfo("CharacteristicWrite 数据: $data") 175 | } 176 | } 177 | 178 | override fun onCharacteristicChanged( 179 | gatt: BluetoothGatt?, 180 | characteristic: BluetoothGattCharacteristic? 181 | ) { 182 | super.onCharacteristicChanged(gatt, characteristic) 183 | characteristic?.let { 184 | val data = String(it.value) 185 | logInfo("CharacteristicChanged 数据: $data") 186 | } 187 | } 188 | 189 | override fun onDescriptorRead( 190 | gatt: BluetoothGatt?, 191 | descriptor: BluetoothGattDescriptor?, 192 | status: Int 193 | ) { 194 | super.onDescriptorRead(gatt, descriptor, status) 195 | descriptor?.let { 196 | val data = String(it.value) 197 | logInfo("DescriptorRead 数据: $data") 198 | } 199 | } 200 | 201 | override fun onDescriptorWrite( 202 | gatt: BluetoothGatt?, 203 | descriptor: BluetoothGattDescriptor?, 204 | status: Int 205 | ) { 206 | super.onDescriptorWrite(gatt, descriptor, status) 207 | descriptor?.let { 208 | val data = String(it.value) 209 | logInfo("DescriptorWrite 数据: $data") 210 | } 211 | } 212 | } 213 | 214 | 215 | /** 216 | * 扫描 217 | */ 218 | fun scan(view: View) { 219 | mData.clear() 220 | mBleAdapter?.notifyDataSetChanged() 221 | BleBlueImpl.scanDev {dev -> 222 | dev.dev.name?.let { 223 | if (dev !in mData) { 224 | mData.add(dev) 225 | mBleAdapter?.notifyItemInserted(mData.size) 226 | } 227 | } 228 | } 229 | } 230 | 231 | 232 | 233 | val sb = StringBuilder() 234 | private fun logInfo(msg:String){ 235 | runOnUiThread { 236 | sb.apply { 237 | append(msg).append("\n") 238 | mInfoTv.text = toString() 239 | } 240 | } 241 | } 242 | 243 | 244 | override fun onDestroy() { 245 | super.onDestroy() 246 | closeConnect() 247 | } 248 | 249 | /** 250 | * 读数据 251 | */ 252 | fun readData(view: View) { 253 | //找到 gatt 服务 254 | val service = getGattService(BleBlueImpl.UUID_SERVICE) 255 | if (service != null) { 256 | val characteristic = 257 | service.getCharacteristic(BleBlueImpl.UUID_READ_NOTIFY) //通过UUID获取可读的Characteristic 258 | mBluetoothGatt?.readCharacteristic(characteristic) 259 | } 260 | } 261 | 262 | 263 | // 获取Gatt服务 264 | private fun getGattService(uuid: UUID): BluetoothGattService? { 265 | if (!isConnected) { 266 | Toast.makeText(this, "没有连接", Toast.LENGTH_SHORT).show() 267 | return null 268 | } 269 | val service = mBluetoothGatt?.getService(uuid) 270 | if (service == null) { 271 | Toast.makeText(this, "没有找到服务", Toast.LENGTH_SHORT).show() 272 | } 273 | return service 274 | } 275 | 276 | fun writeData(view: View) { 277 | val msg = editText.text.toString() 278 | val service = getGattService(BleBlueImpl.UUID_SERVICE) 279 | if (service != null) { 280 | val characteristic = 281 | service.getCharacteristic(BleBlueImpl.UUID_WRITE) //通过UUID获取可读的Characteristic 282 | characteristic.value = msg.toByteArray() 283 | mBluetoothGatt?.writeCharacteristic(characteristic) 284 | } 285 | } 286 | 287 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/ble/BleServerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.ble 2 | 3 | import android.bluetooth.* 4 | import android.bluetooth.le.AdvertiseCallback 5 | import android.bluetooth.le.AdvertiseData 6 | import android.bluetooth.le.AdvertiseSettings 7 | import android.content.Context 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.os.ParcelUuid 11 | import android.util.Log 12 | import android.widget.TextView 13 | import androidx.annotation.RequiresApi 14 | import androidx.appcompat.app.AppCompatActivity 15 | import com.zhengsr.bluetoothdemo.R 16 | 17 | 18 | /** 19 | * @author zhengshaorui 20 | * 外围设备,会不断地发出广播,让中心设备知道,一旦连接上中心设备,就会停止发出广播 21 | * Android 5.0 之后,才能充当外围设备 22 | */ 23 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 24 | class BleServerActivity : AppCompatActivity() { 25 | 26 | 27 | private val TAG = javaClass.simpleName 28 | 29 | private lateinit var textView: TextView 30 | private val mSb = StringBuilder() 31 | private var mBluetoothGattServer: BluetoothGattServer? = null 32 | private var bluetoothAdapter: BluetoothAdapter? = null 33 | override fun onCreate(savedInstanceState: Bundle?) { 34 | super.onCreate(savedInstanceState) 35 | setContentView(R.layout.activity_ble_server) 36 | textView = findViewById(R.id.info) 37 | initBle() 38 | } 39 | 40 | 41 | private fun initBle() { 42 | val blueManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 43 | bluetoothAdapter = blueManager.adapter 44 | bluetoothAdapter?.name = "k20" 45 | 46 | /** 47 | * GAP广播数据最长只能31个字节,包含两中: 广播数据和扫描回复 48 | * - 广播数据是必须的,外设需要不断发送广播,让中心设备知道 49 | * - 扫描回复是可选的,当中心设备扫描到才会扫描回复 50 | * 广播间隔越长,越省电 51 | */ 52 | 53 | //广播设置 54 | val advSetting = AdvertiseSettings.Builder() 55 | //低延时,高功率,不使用后台 56 | .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) 57 | // 高的发送功率 58 | .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) 59 | // 可连接 60 | .setConnectable(true) 61 | //广播时限。最多180000毫秒。值为0将禁用时间限制。(不设置则为无限广播时长) 62 | .setTimeout(0) 63 | .build() 64 | //设置广播包,这个是必须要设置的 65 | val advData = AdvertiseData.Builder() 66 | .setIncludeDeviceName(true) //显示名字 67 | .setIncludeTxPowerLevel(true)//设置功率 68 | .addServiceUuid(ParcelUuid(BleBlueImpl.UUID_SERVICE)) //设置 UUID 服务的 uuid 69 | .build() 70 | 71 | 72 | 73 | //测试 31bit 74 | val byteData = byteArrayOf(-65, 2, 3, 6, 4, 23, 23, 9, 9, 75 | 9,1, 2, 3, 6, 4, 23, 23, 9, 9, 8,23,23,23) 76 | 77 | //扫描广播数据(可不写,客户端扫描才发送) 78 | val scanResponse = AdvertiseData.Builder() 79 | //设置厂商数据 80 | .addManufacturerData(0x19, byteData) 81 | .build() 82 | 83 | 84 | /** 85 | * GATT 使用了 ATT 协议,ATT 把 service 和 characteristic 对应的数据保存在一个查询表中, 86 | * 依次查找每一项的索引 87 | * BLE 设备通过 Service 和 Characteristic 进行通信 88 | * 外设只能被一个中心设备连接,一旦连接,就会停止广播,断开又会重新发送 89 | * 但中心设备同时可以和多个外设连接 90 | * 他们之间需要双向通信的话,唯一的方式就是建立 GATT 连接 91 | * 外设作为 GATT(server),它维持了 ATT 的查找表以及service 和 charateristic 的定义 92 | */ 93 | 94 | val bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser 95 | //开启广播,这个外设就开始发送广播了 96 | bluetoothLeAdvertiser?.startAdvertising( 97 | advSetting, 98 | advData, 99 | scanResponse, 100 | advertiseCallback 101 | ) 102 | 103 | 104 | 105 | 106 | 107 | /** 108 | * characteristic 是最小的逻辑单元 109 | * 一个 characteristic 包含一个单一 value 变量 和 0-n个用来描述 characteristic 变量的 110 | * Descriptor。与 service 相似,每个 characteristic 用 16bit或者32bit的uuid作为标识 111 | * 实际的通信中,也是通过 Characteristic 进行读写通信的 112 | */ 113 | //添加读+通知的 GattCharacteristic 114 | val readCharacteristic = BluetoothGattCharacteristic( 115 | BleBlueImpl.UUID_READ_NOTIFY, 116 | BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_NOTIFY, 117 | BluetoothGattCharacteristic.PERMISSION_READ 118 | ) 119 | 120 | 121 | //添加写的 GattCharacteristic 122 | val writeCharacteristic = BluetoothGattCharacteristic( 123 | BleBlueImpl.UUID_WRITE, 124 | BluetoothGattCharacteristic.PROPERTY_WRITE, 125 | BluetoothGattCharacteristic.PERMISSION_WRITE 126 | ) 127 | 128 | //添加 Descriptor 描述符 129 | val descriptor = 130 | BluetoothGattDescriptor( 131 | BleBlueImpl.UUID_DESCRIBE, 132 | BluetoothGattDescriptor.PERMISSION_WRITE 133 | ) 134 | 135 | //为特征值添加描述 136 | writeCharacteristic.addDescriptor(descriptor) 137 | 138 | 139 | /** 140 | * 添加 Gatt service 用来通信 141 | */ 142 | 143 | //开启广播service,这样才能通信,包含一个或多个 characteristic ,每个service 都有一个 uuid 144 | val gattService = 145 | BluetoothGattService( 146 | BleBlueImpl.UUID_SERVICE, 147 | BluetoothGattService.SERVICE_TYPE_PRIMARY 148 | ) 149 | gattService.addCharacteristic(readCharacteristic) 150 | gattService.addCharacteristic(writeCharacteristic) 151 | 152 | 153 | val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager 154 | //打开 GATT 服务,方便客户端连接 155 | mBluetoothGattServer = bluetoothManager.openGattServer(this, gattServiceCallbak) 156 | mBluetoothGattServer?.addService(gattService) 157 | 158 | 159 | } 160 | 161 | private val gattServiceCallbak = object : BluetoothGattServerCallback() { 162 | override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) { 163 | super.onConnectionStateChange(device, status, newState) 164 | device ?: return 165 | Log.d(TAG, "zsr onConnectionStateChange: ") 166 | if (status == BluetoothGatt.GATT_SUCCESS && newState == 2) { 167 | logInfo("连接到中心设备: ${device?.name}") 168 | } else { 169 | logInfo("与: ${device?.name} 断开连接失败!") 170 | } 171 | } 172 | 173 | 174 | override fun onCharacteristicReadRequest( 175 | device: BluetoothDevice?, 176 | requestId: Int, 177 | offset: Int, 178 | characteristic: BluetoothGattCharacteristic? 179 | ) { 180 | super.onCharacteristicReadRequest(device, requestId, offset, characteristic) 181 | 182 | /** 183 | * 中心设备read时,回调 184 | */ 185 | val data = "this is a test from ble server" 186 | mBluetoothGattServer?.sendResponse( 187 | device, requestId, BluetoothGatt.GATT_SUCCESS, 188 | offset, data.toByteArray() 189 | ) 190 | logInfo("客户端读取 [characteristic ${characteristic?.uuid}] $data") 191 | } 192 | 193 | override fun onCharacteristicWriteRequest( 194 | device: BluetoothDevice?, 195 | requestId: Int, 196 | characteristic: BluetoothGattCharacteristic?, 197 | preparedWrite: Boolean, 198 | responseNeeded: Boolean, 199 | offset: Int, 200 | value: ByteArray? 201 | ) { 202 | super.onCharacteristicWriteRequest( 203 | device, 204 | requestId, 205 | characteristic, 206 | preparedWrite, 207 | responseNeeded, 208 | offset, 209 | value 210 | ) 211 | mBluetoothGattServer?.sendResponse( 212 | device, requestId, BluetoothGatt.GATT_SUCCESS, 213 | offset, value 214 | ) 215 | value?.let { 216 | logInfo("客户端写入 [characteristic ${characteristic?.uuid}] ${String(it)}") 217 | } 218 | } 219 | 220 | override fun onDescriptorReadRequest( 221 | device: BluetoothDevice?, 222 | requestId: Int, 223 | offset: Int, 224 | descriptor: BluetoothGattDescriptor? 225 | ) { 226 | super.onDescriptorReadRequest(device, requestId, offset, descriptor) 227 | val data = "this is a test" 228 | mBluetoothGattServer?.sendResponse( 229 | device, requestId, BluetoothGatt.GATT_SUCCESS, 230 | offset, data.toByteArray() 231 | ) 232 | logInfo("客户端读取 [descriptor ${descriptor?.uuid}] $data") 233 | } 234 | 235 | override fun onDescriptorWriteRequest( 236 | device: BluetoothDevice?, 237 | requestId: Int, 238 | descriptor: BluetoothGattDescriptor?, 239 | preparedWrite: Boolean, 240 | responseNeeded: Boolean, 241 | offset: Int, 242 | value: ByteArray? 243 | ) { 244 | super.onDescriptorWriteRequest( 245 | device, 246 | requestId, 247 | descriptor, 248 | preparedWrite, 249 | responseNeeded, 250 | offset, 251 | value 252 | ) 253 | 254 | value?.let { 255 | logInfo("客户端写入 [descriptor ${descriptor?.uuid}] ${String(it)}") 256 | // 简单模拟通知客户端Characteristic变化 257 | Log.d(TAG, "zsr onDescriptorWriteRequest: $value") 258 | } 259 | 260 | 261 | } 262 | 263 | override fun onExecuteWrite(device: BluetoothDevice?, requestId: Int, execute: Boolean) { 264 | super.onExecuteWrite(device, requestId, execute) 265 | Log.d(TAG, "zsr onExecuteWrite: ") 266 | } 267 | 268 | override fun onNotificationSent(device: BluetoothDevice?, status: Int) { 269 | super.onNotificationSent(device, status) 270 | Log.d(TAG, "zsr onNotificationSent: ") 271 | } 272 | 273 | override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { 274 | super.onMtuChanged(device, mtu) 275 | Log.d(TAG, "zsr onMtuChanged: ") 276 | } 277 | } 278 | 279 | 280 | private val advertiseCallback = object : AdvertiseCallback() { 281 | override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) { 282 | super.onStartSuccess(settingsInEffect) 283 | logInfo("服务准备就绪,请搜索广播") 284 | } 285 | 286 | override fun onStartFailure(errorCode: Int) { 287 | super.onStartFailure(errorCode) 288 | if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) { 289 | logInfo("广播数据超过31个字节了 !") 290 | } else { 291 | logInfo("服务启动失败: $errorCode") 292 | } 293 | } 294 | } 295 | 296 | private fun logInfo(msg: String) { 297 | runOnUiThread { 298 | mSb.apply { 299 | append(msg).append("\n") 300 | textView.text = toString() 301 | } 302 | } 303 | } 304 | 305 | override fun onDestroy() { 306 | super.onDestroy() 307 | bluetoothAdapter?.bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback) 308 | mBluetoothGattServer?.close() 309 | } 310 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/bt/BtBlueImpl.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.bt 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.bluetooth.BluetoothDevice 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.Job 11 | import kotlinx.coroutines.launch 12 | import java.util.* 13 | 14 | /** 15 | * @author by zhengshaorui 2020/8/5 09:26 16 | * describe:实现一些基本功能和接口 17 | */ 18 | typealias BlueDevFoundListener = (BluetoothDevice) -> Unit 19 | typealias BlueBroadcastListener = (context: Context?, intent: Intent?) -> Unit 20 | 21 | 22 | object BtBlueImpl { 23 | val BLUE_UUID = UUID.fromString("00001101-2300-1000-8000-00815F9B34FB") 24 | 25 | 26 | private var context: Context? = null 27 | fun init(context: Context?): BtBlueImpl { 28 | this.context = context?.applicationContext 29 | return this 30 | } 31 | 32 | private val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() 33 | 34 | private var blueBroadcastListener: BlueBroadcastListener? = null 35 | private var blueDevFoundListener: BlueDevFoundListener? = null 36 | 37 | private var blueBroadcast: BlueBroadcast? = null 38 | 39 | private val job = Job() 40 | private val scope = CoroutineScope(job) 41 | 42 | /** 43 | * 注册广播 44 | */ 45 | fun registerBroadcast( 46 | actions: List? = null, 47 | callback: BlueBroadcastListener? = null 48 | ): BtBlueImpl { 49 | blueBroadcastListener = callback 50 | IntentFilter().apply { 51 | addAction(BluetoothDevice.ACTION_FOUND) 52 | addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED) 53 | blueBroadcast = BlueBroadcast() 54 | context?.registerReceiver(blueBroadcast, this) 55 | } 56 | return this 57 | } 58 | 59 | /** 60 | * 蓝牙广播接收 61 | */ 62 | class BlueBroadcast : BroadcastReceiver() { 63 | override fun onReceive(context: Context?, intent: Intent?) { 64 | blueBroadcastListener?.let { it(context, intent) } 65 | when (intent?.action) { 66 | BluetoothDevice.ACTION_FOUND -> { 67 | val device = 68 | intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) 69 | device ?: return 70 | blueDevFoundListener?.let { it(device) } 71 | 72 | } 73 | 74 | } 75 | } 76 | 77 | } 78 | 79 | /** 80 | * 查找蓝牙 81 | */ 82 | fun foundDevices(callback: BlueDevFoundListener?) { 83 | scope.launch { 84 | val time = System.currentTimeMillis() 85 | blueDevFoundListener = callback; 86 | //先取消搜索 87 | bluetoothAdapter.cancelDiscovery() 88 | 89 | //获取已经配对的设备 90 | val bondedDevices = bluetoothAdapter.bondedDevices 91 | bondedDevices?.forEach { device -> 92 | //公布给外面,方便 recyclerview 等设备连接 93 | callback?.let { it(device) } 94 | } 95 | val now = System.currentTimeMillis() - time; 96 | //搜索蓝牙,这个过程大概12s左右 97 | bluetoothAdapter.startDiscovery() 98 | 99 | } 100 | 101 | 102 | } 103 | 104 | /** 105 | * 注册需要的广播 106 | */ 107 | // abstract fun initBroadcast(): IntentFilter? 108 | 109 | /** 110 | * 拿到蓝牙类 111 | */ 112 | fun getBluetooth(): BluetoothAdapter { 113 | return bluetoothAdapter 114 | } 115 | 116 | /** 117 | * 释放资源 118 | */ 119 | fun release() { 120 | blueBroadcast?.let { context?.unregisterReceiver(it) } 121 | } 122 | 123 | 124 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/bt/BtClientActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.bt 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.bluetooth.BluetoothDevice 5 | import android.bluetooth.BluetoothSocket 6 | import android.graphics.Color 7 | import android.os.Bundle 8 | import android.view.View 9 | import android.widget.EditText 10 | import android.widget.TextView 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.recyclerview.widget.LinearLayoutManager 14 | import androidx.recyclerview.widget.RecyclerView 15 | import com.chad.library.adapter.base.BaseQuickAdapter 16 | import com.chad.library.adapter.base.listener.OnItemClickListener 17 | import com.chad.library.adapter.base.listener.OnItemLongClickListener 18 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 19 | import com.zhengsr.bluetoothdemo.R 20 | import kotlinx.coroutines.CoroutineScope 21 | import kotlinx.coroutines.Job 22 | 23 | class BtClientActivity : AppCompatActivity(), OnItemClickListener, OnItemLongClickListener { 24 | 25 | companion object{ 26 | private val TAG =javaClass.simpleName 27 | } 28 | /** 29 | * UI 30 | */ 31 | private var itemStateTv: TextView? = null 32 | private lateinit var logTv: TextView 33 | private lateinit var sendMsgEd:EditText 34 | 35 | /** 36 | * logic 37 | */ 38 | private var blueBeans: MutableList = mutableListOf() 39 | private lateinit var blueAdapter: BlueAdapter 40 | private lateinit var bluetooth: BluetoothAdapter 41 | private val stringBuilder = StringBuilder() 42 | private var connectThread: ConnectThread? = null 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | setContentView(R.layout.activity_bl_client) 47 | 48 | bluetooth = BluetoothAdapter.getDefaultAdapter() 49 | logTv = findViewById(R.id.tv_log) 50 | sendMsgEd = findViewById(R.id.send_edit) 51 | 52 | initRecyclerView() 53 | BtBlueImpl.init(this) 54 | .registerBroadcast() 55 | .foundDevices { dev -> 56 | 57 | if (dev !in blueBeans && dev.name != null) { 58 | blueBeans.add(dev) 59 | blueAdapter.notifyItemInserted(blueBeans.size) 60 | } 61 | } 62 | } 63 | 64 | 65 | /** 66 | * 初始化 recyclerview 67 | */ 68 | private fun initRecyclerView() { 69 | val recyclerView: RecyclerView = findViewById(R.id.recycler) 70 | val manager = LinearLayoutManager(this) 71 | 72 | recyclerView.layoutManager = manager 73 | blueAdapter = 74 | BlueAdapter( 75 | blueBeans, 76 | R.layout.recy_blue_item_layout 77 | ) 78 | blueAdapter.animationEnable = true 79 | recyclerView.adapter = blueAdapter 80 | 81 | blueAdapter.setOnItemClickListener(this) 82 | blueAdapter.setOnItemLongClickListener(this) 83 | } 84 | 85 | 86 | /** 87 | * recyclerview 的 adapter 88 | */ 89 | class BlueAdapter(datas: MutableList, layoutResId: Int) : 90 | BaseQuickAdapter(layoutResId, datas) { 91 | override fun convert(holder: BaseViewHolder, item: BluetoothDevice) { 92 | 93 | holder.setText(R.id.blue_item_addr_tv, item.address) 94 | holder.setText(R.id.blue_item_name_tv, item.name) 95 | 96 | val statusTv = holder.getView(R.id.blue_item_status_tv) 97 | if (item.bondState == BluetoothDevice.BOND_BONDED) { 98 | statusTv.text = "(已配对)" 99 | statusTv.setTextColor(Color.parseColor("#ff009688")) 100 | } else { 101 | statusTv.text = "(未配对)" 102 | statusTv.setTextColor(Color.parseColor("#ffFF5722")) 103 | 104 | } 105 | } 106 | 107 | } 108 | 109 | override fun onItemClick(adapter: BaseQuickAdapter<*, *>, view: View, position: Int) { 110 | val dev: BluetoothDevice = blueBeans[position] 111 | Toast.makeText(this, "开始连接...", Toast.LENGTH_SHORT).show() 112 | itemStateTv = view.findViewById(R.id.blue_item_status_tv) 113 | connectThread = ConnectThread(dev, readListener, writeListener) 114 | connectThread?.start() 115 | } 116 | 117 | override fun onItemLongClick( 118 | adapter: BaseQuickAdapter<*, *>, 119 | view: View, 120 | position: Int 121 | ): Boolean { 122 | //todo 解绑估计需要系统 api,后面看看源码是怎么实现的 123 | 124 | return true 125 | } 126 | 127 | 128 | 129 | 130 | /** 131 | * 扫描蓝牙 132 | */ 133 | fun scan(view: View) { 134 | blueBeans.clear() 135 | blueAdapter.notifyDataSetChanged() 136 | BtBlueImpl.foundDevices { bean -> 137 | if (bean !in blueBeans && bean.name != null) { 138 | blueBeans.add(bean) 139 | blueAdapter.notifyItemInserted(blueBeans.size) 140 | } 141 | } 142 | } 143 | 144 | 145 | val job = Job() 146 | val coroutineScope = CoroutineScope(job) 147 | 148 | /** 149 | * 连接类 150 | */ 151 | 152 | inner class ConnectThread( 153 | val device: BluetoothDevice, val readListener: HandleSocket.BluetoothListener?, 154 | val writeListener: HandleSocket.BaseBluetoothListener? 155 | ) : Thread() { 156 | var handleSocket: HandleSocket? = null 157 | private val socket: BluetoothSocket? by lazy { 158 | readListener?.onStart() 159 | //监听该 uuid 160 | device.createRfcommSocketToServiceRecord(BtBlueImpl.BLUE_UUID) 161 | } 162 | 163 | override fun run() { 164 | super.run() 165 | //下取消 166 | bluetooth.cancelDiscovery() 167 | try { 168 | 169 | socket.run { 170 | //阻塞等待 171 | this?.connect() 172 | //连接成功,拿到服务端设备名 173 | socket?.remoteDevice?.let { readListener?.onConnected(it.name) } 174 | 175 | //处理 socket 读写 176 | handleSocket = 177 | HandleSocket(this) 178 | handleSocket?.start(readListener, writeListener) 179 | 180 | } 181 | } catch (e: Exception) { 182 | readListener?.onFail(e.message.toString()) 183 | } 184 | } 185 | 186 | fun cancel() { 187 | socket?.close() 188 | handleSocket?.cancel() 189 | } 190 | } 191 | 192 | 193 | fun sendMsg(view: View) { 194 | connectThread?.handleSocket?.sendMsg(sendMsgEd.text.toString()) 195 | sendMsgEd.setText("") 196 | 197 | } 198 | 199 | override fun onDestroy() { 200 | super.onDestroy() 201 | connectThread?.cancel() 202 | BtBlueImpl.release() 203 | } 204 | 205 | val readListener = object : HandleSocket.BluetoothListener { 206 | override fun onStart() { 207 | runOnUiThread { 208 | itemStateTv?.text = "正在连接..." 209 | } 210 | } 211 | 212 | override fun onReceiveData(socket: BluetoothSocket?,msg: String) { 213 | runOnUiThread { 214 | logTv.text = stringBuilder.run { 215 | append(socket?.remoteDevice?.name+": "+msg).append("\n") 216 | toString() 217 | } 218 | } 219 | } 220 | 221 | override fun onConnected(msg: String) { 222 | super.onConnected(msg) 223 | runOnUiThread { 224 | itemStateTv?.text = "已连接" 225 | } 226 | } 227 | 228 | override fun onFail(error: String) { 229 | runOnUiThread { 230 | logTv.text = stringBuilder.run { 231 | append(error).append("\n") 232 | toString() 233 | } 234 | itemStateTv?.text = "已配对" 235 | } 236 | } 237 | 238 | } 239 | val writeListener = object : HandleSocket.BaseBluetoothListener { 240 | 241 | override fun onsendMsg(socket: BluetoothSocket?, msg: String) { 242 | runOnUiThread { 243 | logTv.text = stringBuilder.run { 244 | append("我: $msg").append("\n") 245 | toString() 246 | } 247 | } 248 | } 249 | override fun onFail(error: String) { 250 | // Log.d(com.zhengsr.bluetoothdemo.TAG, "zsr write onFail: $error") 251 | logTv.text = stringBuilder.run { 252 | append("发送失败: $error").append("\n") 253 | toString() 254 | } 255 | } 256 | 257 | } 258 | 259 | 260 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/bt/BtServerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.bt 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.bluetooth.BluetoothServerSocket 5 | import android.bluetooth.BluetoothSocket 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.View 9 | import android.widget.EditText 10 | import android.widget.TextView 11 | import androidx.appcompat.app.AppCompatActivity 12 | import com.zhengsr.bluetoothdemo.R 13 | 14 | class BtServerActivity : AppCompatActivity() { 15 | companion object{ 16 | private val TAG = javaClass.simpleName 17 | } 18 | 19 | /** 20 | * logic 21 | */ 22 | private lateinit var bluetooth: BluetoothAdapter 23 | private lateinit var socketThread: AcceptThread 24 | private lateinit var handleSocket: HandleSocket 25 | 26 | /** 27 | * UI 28 | */ 29 | private lateinit var serverStatusTv: TextView 30 | private lateinit var logTv: TextView 31 | private lateinit var sendMsgEd: EditText 32 | 33 | val stringBuffer = StringBuilder(); 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | setContentView(R.layout.activity_bl_server) 37 | sendMsgEd = findViewById(R.id.send_edit) 38 | serverStatusTv = findViewById(R.id.servertv) 39 | logTv = findViewById(R.id.tv_log) 40 | 41 | bluetooth = BluetoothAdapter.getDefaultAdapter(); 42 | socketThread = AcceptThread(readListener,writeListener) 43 | socketThread.start() 44 | 45 | 46 | } 47 | 48 | 49 | val readListener = object : HandleSocket.BluetoothListener { 50 | override fun onStart() { 51 | runOnUiThread{ 52 | serverStatusTv.text = "服务器已就绪.." 53 | } 54 | } 55 | 56 | override fun onConnected(msg: String) { 57 | super.onConnected(msg) 58 | runOnUiThread{ 59 | serverStatusTv.text = "已连接上客户端: $msg" 60 | } 61 | } 62 | 63 | 64 | 65 | 66 | override fun onReceiveData(socket: BluetoothSocket?,msg: String) { 67 | runOnUiThread { 68 | logTv.text = stringBuffer.run { 69 | append(socket?.remoteDevice?.name+": "+msg).append("\n") 70 | toString() 71 | } 72 | } 73 | } 74 | 75 | override fun onFail(error: String) { 76 | runOnUiThread{ 77 | serverStatusTv.text = error 78 | } 79 | } 80 | 81 | } 82 | val writeListener = object : HandleSocket.BaseBluetoothListener { 83 | override fun onsendMsg(socket: BluetoothSocket?, msg: String) { 84 | runOnUiThread { 85 | logTv.text = stringBuffer.run { 86 | append("我: $msg").append("\n") 87 | toString() 88 | } 89 | } 90 | } 91 | 92 | 93 | override fun onFail(error: String) { 94 | Log.d(TAG, "zsr write onFail: $error") 95 | } 96 | 97 | } 98 | 99 | /** 100 | * 监听是否有设备接入 101 | */ 102 | private inner class AcceptThread(val readListener: HandleSocket.BluetoothListener?, 103 | val writeListener: HandleSocket.BaseBluetoothListener?) : Thread() { 104 | 105 | 106 | private val serverSocket: BluetoothServerSocket? by lazy { 107 | //非明文匹配,不安全 108 | readListener?.onStart() 109 | bluetooth.listenUsingInsecureRfcommWithServiceRecord(TAG, BtBlueImpl.BLUE_UUID) 110 | } 111 | 112 | override fun run() { 113 | super.run() 114 | var shouldLoop = true 115 | while (shouldLoop) { 116 | var socket: BluetoothSocket? = 117 | try { 118 | //监听是否有接入 119 | serverSocket?.accept() 120 | } catch (e: Exception) { 121 | Log.d(TAG, "zsr blue socket accept fail: ${e.message}") 122 | shouldLoop = false 123 | null 124 | } 125 | socket?.also { 126 | //拿到接入设备的名字 127 | readListener?.onConnected(socket.remoteDevice.name) 128 | //处理接收事件 129 | handleSocket = 130 | HandleSocket(socket) 131 | handleSocket.start(readListener,writeListener) 132 | //关闭服务端,只连接一个 133 | serverSocket?.close() 134 | shouldLoop = false; 135 | } 136 | } 137 | } 138 | 139 | fun cancel() { 140 | serverSocket?.close() 141 | handleSocket.cancel() 142 | } 143 | } 144 | 145 | 146 | override fun onDestroy() { 147 | super.onDestroy() 148 | if (!::socketThread.isInitialized) { 149 | socketThread.cancel() 150 | } 151 | 152 | } 153 | 154 | fun sendMsg(view: View) { 155 | if (::handleSocket.isInitialized) { 156 | handleSocket.sendMsg(sendMsgEd.text.toString()) 157 | sendMsgEd.setText("") 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/bluetooth/bt/HandleSocket.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.bluetooth.bt 2 | 3 | import android.bluetooth.BluetoothSocket 4 | import com.zhengsr.bluetoothdemo.utils.close 5 | import kotlinx.coroutines.* 6 | import java.io.DataInputStream 7 | import java.io.OutputStream 8 | 9 | /** 10 | * @author by zhengshaorui 2020/7/29 16:46 11 | * describe:BluetoothSocket 处理读写时间 12 | */ 13 | class HandleSocket(private val socket: BluetoothSocket?) { 14 | private lateinit var readThread: ReadThread 15 | private lateinit var writeThread: WriteThread 16 | 17 | companion object { 18 | private val TAG = HandleSocket::class.java.simpleName 19 | } 20 | 21 | 22 | fun start( 23 | readlisterner: BluetoothListener?, 24 | writelistener: BaseBluetoothListener? 25 | ) { 26 | readThread = ReadThread( 27 | socket, 28 | readlisterner 29 | ) 30 | readThread.start() 31 | 32 | writeThread = WriteThread(socket, writelistener) 33 | } 34 | 35 | 36 | /** 37 | * 读取数据 38 | */ 39 | private class ReadThread( 40 | val socket: BluetoothSocket?, 41 | val bluetoothListener: BaseBluetoothListener? 42 | ) : Thread() { 43 | 44 | //拿到 BluetoothSocket 的输入流 45 | private val inputStream: DataInputStream? = DataInputStream(socket?.inputStream) 46 | private var isDone = false 47 | private val listener: BluetoothListener? = 48 | bluetoothListener as BluetoothListener 49 | 50 | //todo 目前简单数据,暂时使用这种 51 | private val byteBuffer: ByteArray = ByteArray(1024) 52 | override fun run() { 53 | super.run() 54 | var size: Int? = null 55 | while (!isDone) { 56 | try { 57 | //拿到读的数据和大小 58 | size = inputStream?.read(byteBuffer) 59 | } catch (e: Exception) { 60 | isDone = false 61 | e.message?.let { listener?.onFail(it) } 62 | return 63 | } 64 | 65 | 66 | if (size != null && size > 0) { 67 | //把结果公布出去 68 | listener?.onReceiveData(socket,String(byteBuffer, 0, size)) 69 | } else { 70 | //如果接收不到数据,则证明已经断开了 71 | listener?.onFail("断开连接") 72 | isDone = false; 73 | } 74 | } 75 | } 76 | 77 | fun cancel() { 78 | isDone = false; 79 | socket?.close() 80 | close(inputStream) 81 | } 82 | } 83 | 84 | /** 85 | * 写数据 86 | */ 87 | private val job = Job() 88 | private val scope = CoroutineScope(job) 89 | 90 | inner class WriteThread( 91 | private val socket: BluetoothSocket?, 92 | val listener: BaseBluetoothListener? 93 | ) { 94 | 95 | private var isDone = false 96 | 97 | //拿到 socket 的 outputstream 98 | private val dataOutput: OutputStream? = socket?.outputStream 99 | 100 | fun sendMsg(msg: String) { 101 | if (isDone) { 102 | return 103 | } 104 | scope.launch(Dispatchers.Main) { 105 | val result = withContext(Dispatchers.IO) { 106 | sendScope(msg) 107 | } 108 | 109 | if (result != null) { 110 | listener?.onFail(result) 111 | }else{ 112 | listener?.onsendMsg(socket,msg) 113 | } 114 | 115 | } 116 | } 117 | 118 | //实际发送的类 119 | private fun sendScope(msg: String): String? { 120 | return try { 121 | //写数据 122 | dataOutput?.write(msg.toByteArray()) 123 | dataOutput?.flush() 124 | null 125 | } catch (e: Exception) { 126 | e.toString() 127 | } 128 | } 129 | 130 | fun cancel() { 131 | isDone = true 132 | socket?.close() 133 | close(dataOutput) 134 | } 135 | 136 | 137 | } 138 | 139 | fun sendMsg(string: String) { 140 | writeThread.sendMsg(string) 141 | } 142 | 143 | interface BaseBluetoothListener { 144 | fun onsendMsg(socket: BluetoothSocket?,msg: String){} 145 | fun onFail(error: String) 146 | } 147 | 148 | 149 | interface BluetoothListener : 150 | BaseBluetoothListener { 151 | fun onStart() 152 | fun onReceiveData(socket: BluetoothSocket?,msg: String) 153 | fun onConnected(msg: String) {} 154 | 155 | } 156 | 157 | /** 158 | * 关闭连接 159 | */ 160 | fun cancel() { 161 | readThread?.cancel() 162 | writeThread?.cancel() 163 | close(socket) 164 | job.cancel() 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/utils/CloseUtils.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.utils 2 | 3 | import java.io.Closeable 4 | 5 | /** 6 | * @author by zhengshaorui 2020/7/29 17:26 7 | * describe:关闭类,支持 closeable 8 | */ 9 | 10 | fun close(vararg closeable:Closeable?){ 11 | closeable?.forEach { 12 | obj -> 13 | obj?.close() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zhengsr/bluetoothdemo/utils/Tool.kt: -------------------------------------------------------------------------------- 1 | package com.zhengsr.bluetoothdemo.utils 2 | 3 | /** 4 | * @author by zhengshaorui 2020/8/27 10:31 5 | * describe:工具类,都用顶层函数 6 | */ 7 | 8 | /** 9 | * 数组转十六进制字符串 10 | */ 11 | internal fun bytesToHexString(src: ByteArray): String { 12 | val stringBuilder = StringBuilder("") 13 | for (element in src) { 14 | val v = element.toInt() and 0xFF 15 | val hv = Integer.toHexString(v) 16 | if (hv.length < 2) { 17 | stringBuilder.append(0) 18 | } 19 | stringBuilder.append(hv) 20 | } 21 | return stringBuilder.toString() 22 | } 23 | 24 | /** 25 | * 十六进制字符串转字符数组 26 | */ 27 | fun hexStringToBytes(hexString: String): ByteArray { 28 | var hexString = hexString 29 | hexString = hexString.toUpperCase() 30 | val length = hexString.length / 2 31 | val hexChars = hexString.toCharArray() 32 | val d = ByteArray(length) 33 | for (i in 0..length - 1) { 34 | val pos = i * 2 35 | d[i] = (charToByte(hexChars[pos]).toInt() shl 4 or charToByte(hexChars[pos + 1]).toInt()).toByte() 36 | } 37 | return d 38 | } 39 | 40 | /** 41 | * Convert char to byte 42 | * @param c char 43 | * * 44 | * @return byte 45 | */ 46 | private fun charToByte(c: Char): Byte { 47 | 48 | return "0123456789ABCDEF".indexOf(c).toByte() 49 | } 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_a2dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |