├── .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 |
5 |
6 |
7 |
8 |
9 |
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 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
16 |
17 |
18 |
23 |
24 |
27 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_bl_client.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
22 |
23 |
29 |
30 |
35 |
38 |
39 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_bl_server.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
18 |
19 |
26 |
27 |
32 |
33 |
36 |
37 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_ble_client.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
20 |
21 |
26 |
27 |
32 |
37 |
38 |
41 |
42 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_ble_server.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
23 |
29 |
30 |
36 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/recy_ble_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
32 |
42 |
43 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/recy_blue_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
20 |
21 |
31 |
32 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | BluetoothDemo
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/test/java/com/zhengsr/bluetoothdemo/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.zhengsr.bluetoothdemo
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.3.72"
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:4.0.1"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | maven { url 'https://jitpack.io' }
22 | }
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LillteZheng/BluetoothDemo/455a64d9ac6021f1a3952b4268e2249b9f2df4f9/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 04 15:24:25 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "BluetoothDemo"
--------------------------------------------------------------------------------