1 /*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package android.bluetooth
17
18 import android.Manifest
19 import android.content.BroadcastReceiver
20 import android.content.Context
21 import android.content.Intent
22 import android.content.IntentFilter
23 import android.util.Log
24 import androidx.test.core.app.ApplicationProvider
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.platform.app.InstrumentationRegistry
27 import com.android.compatibility.common.util.AdoptShellPermissionsRule
28 import com.google.common.truth.Truth
29 import com.google.protobuf.ByteString
30 import io.grpc.stub.StreamObserver
31 import java.time.Duration
32 import java.util.UUID
33 import java.util.concurrent.TimeUnit
34 import kotlinx.coroutines.ExperimentalCoroutinesApi
35 import kotlinx.coroutines.channels.Channel
36 import kotlinx.coroutines.channels.awaitClose
37 import kotlinx.coroutines.channels.trySendBlocking
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.callbackFlow
40 import kotlinx.coroutines.flow.consumeAsFlow
41 import kotlinx.coroutines.flow.first
42 import kotlinx.coroutines.runBlocking
43 import kotlinx.coroutines.withTimeout
44 import org.junit.After
45 import org.junit.Before
46 import org.junit.Rule
47 import org.junit.Test
48 import org.junit.runner.RunWith
49 import pandora.RfcommProto
50 import pandora.RfcommProto.ServerId
51 import pandora.RfcommProto.StartServerRequest
52 import pandora.SecurityProto.PairingEvent
53 import pandora.SecurityProto.PairingEventAnswer
54
55 @kotlinx.coroutines.ExperimentalCoroutinesApi
bondingFlownull56 fun bondingFlow(context: Context, peer: BluetoothDevice, state: Int): Flow<Intent> {
57 val channel = Channel<Intent>(Channel.UNLIMITED)
58 val receiver: BroadcastReceiver =
59 object : BroadcastReceiver() {
60 override fun onReceive(context: Context, intent: Intent) {
61 if (
62 peer ==
63 intent.getParcelableExtra(
64 BluetoothDevice.EXTRA_DEVICE,
65 BluetoothDevice::class.java
66 )
67 ) {
68 if (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1) == state) {
69 channel.trySendBlocking(intent)
70 }
71 }
72 }
73 }
74 context.registerReceiver(receiver, IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED))
75 channel.invokeOnClose { context.unregisterReceiver(receiver) }
76 return channel.consumeAsFlow()
77 }
78
79 class PairingResponder(
80 private val mPeer: BluetoothDevice,
81 private val mPairingEventIterator: Iterator<PairingEvent>,
82 private val mPairingEventAnswerObserver: StreamObserver<PairingEventAnswer>
83 ) : BroadcastReceiver() {
onReceivenull84 override fun onReceive(context: Context, intent: Intent) {
85 when (intent.action) {
86 BluetoothDevice.ACTION_PAIRING_REQUEST -> {
87 if (
88 mPeer ==
89 intent.getParcelableExtra(
90 BluetoothDevice.EXTRA_DEVICE,
91 BluetoothDevice::class.java
92 )
93 ) {
94 if (
95 BluetoothDevice.PAIRING_VARIANT_CONSENT ==
96 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, -1)
97 ) {
98 mPeer.setPairingConfirmation(true)
99 val pairingEvent: PairingEvent = mPairingEventIterator.next()
100 Truth.assertThat(pairingEvent.hasJustWorks()).isTrue()
101 mPairingEventAnswerObserver.onNext(
102 PairingEventAnswer.newBuilder()
103 .setEvent(pairingEvent)
104 .setConfirm(true)
105 .build()
106 )
107 }
108 }
109 }
110 }
111 }
112 }
113
114 @RunWith(AndroidJUnit4::class)
115 class RfcommTest {
116 private val mContext = ApplicationProvider.getApplicationContext<Context>()
117 private val mManager = mContext.getSystemService(BluetoothManager::class.java)
118 private val mAdapter = mManager!!.adapter
119
120 // Gives shell permissions during the test.
121 @Rule
122 @JvmField
123 val mPermissionsRule =
124 AdoptShellPermissionsRule(
125 InstrumentationRegistry.getInstrumentation().getUiAutomation(),
126 Manifest.permission.BLUETOOTH_CONNECT,
127 Manifest.permission.BLUETOOTH_PRIVILEGED
128 )
129
130 // Set up a Bumble Pandora device for the duration of the test.
131 @Rule @JvmField val mBumble = PandoraDevice()
132
133 private lateinit var mBumbleDevice: BluetoothDevice
134 private lateinit var mPairingResponder: PairingResponder
135 private lateinit var mPairingEventAnswerObserver: StreamObserver<PairingEventAnswer>
136 private val mPairingEventStreamObserver: StreamObserverSpliterator<PairingEvent> =
137 StreamObserverSpliterator()
138 private var mConnectionCounter = 1
139
140 @Before
setUpnull141 fun setUp() {
142 mBumbleDevice = mBumble.remoteDevice
143 mPairingEventAnswerObserver =
144 mBumble
145 .security()
146 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
147 .onPairing(mPairingEventStreamObserver)
148
149 val pairingFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
150 mPairingResponder =
151 PairingResponder(
152 mBumbleDevice,
153 mPairingEventStreamObserver.iterator(),
154 mPairingEventAnswerObserver
155 )
156 mContext.registerReceiver(mPairingResponder, pairingFilter)
157
158 // TODO: Ideally we shouldn't need this, remove
159 runBlocking { removeBondIfBonded(mBumbleDevice) }
160 }
161
162 @After
tearDownnull163 fun tearDown() {
164 mContext.unregisterReceiver(mPairingResponder)
165 }
166
167 @Test
clientConnectToOpenServerSocketBondedInsecurenull168 fun clientConnectToOpenServerSocketBondedInsecure() {
169 startServer {
170 val serverId = it
171 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
172
173 // Insecure connection to RFCOMM Server
174 val insecureSocket =
175 mBumbleDevice.createInsecureRfcommSocketToServiceRecord(UUID.fromString(TEST_UUID))
176 insecureSocket.connect()
177
178 val connectionResponse =
179 mBumble
180 .rfcommBlocking()
181 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
182 .acceptConnection(
183 RfcommProto.AcceptConnectionRequest.newBuilder().setServer(serverId).build()
184 )
185 Truth.assertThat(connectionResponse.connection.id).isEqualTo(mConnectionCounter)
186 Truth.assertThat(insecureSocket.isConnected).isTrue()
187 }
188 }
189
190 @Test
clientConnectToOpenServerSocketBondedSecurenull191 fun clientConnectToOpenServerSocketBondedSecure() {
192 startServer {
193 val serverId = it
194 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
195 // Secure connection to RFCOMM Server
196 val secureSocket =
197 mBumbleDevice.createRfcommSocketToServiceRecord(UUID.fromString(TEST_UUID))
198 secureSocket.connect()
199
200 val connectionResponse =
201 mBumble
202 .rfcommBlocking()
203 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
204 .acceptConnection(
205 RfcommProto.AcceptConnectionRequest.newBuilder().setServer(serverId).build()
206 )
207 Truth.assertThat(connectionResponse.connection.id).isEqualTo(mConnectionCounter)
208 Truth.assertThat(secureSocket.isConnected).isTrue()
209 }
210 }
211
212 @Test
clientSendDataOverInsecureSocketnull213 fun clientSendDataOverInsecureSocket() {
214 startServer {
215 val serverId = it
216 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
217
218 val (insecureSocket, connection) = createAndConnectSocket(isSecure = false, serverId)
219 val data: ByteArray = "Test data for clientSendDataOverInsecureSocket".toByteArray()
220 val socketOs = insecureSocket.outputStream
221
222 socketOs.write(data)
223 val rxResponse: RfcommProto.RxResponse =
224 mBumble
225 .rfcommBlocking()
226 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
227 .receive(RfcommProto.RxRequest.newBuilder().setConnection(connection).build())
228 Truth.assertThat(rxResponse.data).isEqualTo(ByteString.copyFrom(data))
229 }
230 }
231
232 @Test
clientSendDataOverSecureSocketnull233 fun clientSendDataOverSecureSocket() {
234 startServer {
235 val serverId = it
236 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
237
238 val (secureSocket, connection) = createAndConnectSocket(isSecure = true, serverId)
239 val data: ByteArray = "Test data for clientSendDataOverSecureSocket".toByteArray()
240 val socketOs = secureSocket.outputStream
241
242 socketOs.write(data)
243 val rxResponse: RfcommProto.RxResponse =
244 mBumble
245 .rfcommBlocking()
246 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
247 .receive(RfcommProto.RxRequest.newBuilder().setConnection(connection).build())
248 Truth.assertThat(rxResponse.data).isEqualTo(ByteString.copyFrom(data))
249 }
250 }
251
252 @Test
clientReceiveDataOverInsecureSocketnull253 fun clientReceiveDataOverInsecureSocket() {
254 startServer {
255 val serverId = it
256 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
257
258 val (insecureSocket, connection) = createAndConnectSocket(isSecure = false, serverId)
259 val buffer = ByteArray(64)
260 val socketIs = insecureSocket.inputStream
261 val data: ByteString =
262 ByteString.copyFromUtf8("Test data for clientReceiveDataOverInsecureSocket")
263
264 val txRequest =
265 RfcommProto.TxRequest.newBuilder().setConnection(connection).setData(data).build()
266 mBumble.rfcommBlocking().send(txRequest)
267 val numBytesFromBumble = socketIs.read(buffer)
268 Truth.assertThat(ByteString.copyFrom(buffer).substring(0, numBytesFromBumble))
269 .isEqualTo(data)
270 }
271 }
272
273 @Test
clientReceiveDataOverSecureSocketnull274 fun clientReceiveDataOverSecureSocket() {
275 startServer {
276 val serverId = it
277 runBlocking { withTimeout(BOND_TIMEOUT.toMillis()) { bondDevice(mBumbleDevice) } }
278
279 val (secureSocket, connection) = createAndConnectSocket(isSecure = true, serverId)
280 val buffer = ByteArray(64)
281 val socketIs = secureSocket.inputStream
282 val data: ByteString =
283 ByteString.copyFromUtf8("Test data for clientReceiveDataOverSecureSocket")
284
285 val txRequest =
286 RfcommProto.TxRequest.newBuilder().setConnection(connection).setData(data).build()
287 mBumble.rfcommBlocking().send(txRequest)
288 val numBytesFromBumble = socketIs.read(buffer)
289 Truth.assertThat(ByteString.copyFrom(buffer).substring(0, numBytesFromBumble))
290 .isEqualTo(data)
291 }
292 }
293
createAndConnectSocketnull294 private fun createAndConnectSocket(
295 isSecure: Boolean,
296 server: ServerId
297 ): Pair<BluetoothSocket, RfcommProto.RfcommConnection> {
298 val socket =
299 if (isSecure) {
300 mBumbleDevice.createRfcommSocketToServiceRecord(UUID.fromString(TEST_UUID))
301 } else {
302 mBumbleDevice.createInsecureRfcommSocketToServiceRecord(UUID.fromString(TEST_UUID))
303 }
304 socket.connect()
305
306 val connectionResponse =
307 mBumble
308 .rfcommBlocking()
309 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
310 .acceptConnection(
311 RfcommProto.AcceptConnectionRequest.newBuilder().setServer(server).build()
312 )
313 Truth.assertThat(connectionResponse.connection.id).isEqualTo(mConnectionCounter)
314 Truth.assertThat(socket.isConnected).isTrue()
315
316 mConnectionCounter += 1
317 val connection = connectionResponse.connection
318 return Pair(socket, connection)
319 }
320
321 @OptIn(ExperimentalCoroutinesApi::class)
bondDevicenull322 private suspend fun bondDevice(remoteDevice: BluetoothDevice) {
323 // TODO: b/345842833
324 // HFP will try to connect, and bumble doesn't support HFP yet
325 disableHfp()
326
327 if (mAdapter.bondedDevices.contains(remoteDevice)) {
328 Log.d(TAG, "bondDevice(): The device is already bonded")
329 return
330 }
331
332 val flow = bondingFlow(mContext, remoteDevice, BluetoothDevice.BOND_BONDED)
333
334 Truth.assertThat(remoteDevice.createBond()).isTrue()
335
336 flow.first()
337 }
338
339 @OptIn(ExperimentalCoroutinesApi::class)
removeBondIfBondednull340 private suspend fun removeBondIfBonded(deviceToRemove: BluetoothDevice) {
341 if (!mAdapter.bondedDevices.contains(deviceToRemove)) {
342 Log.d(TAG, "removeBondIfBonded(): Tried to remove a device that isn't bonded")
343 return
344 }
345 val flow = bondingFlow(mContext, deviceToRemove, BluetoothDevice.BOND_NONE)
346
347 Truth.assertThat(deviceToRemove.removeBond()).isTrue()
348
349 flow.first()
350 }
351
startServernull352 private fun startServer(block: (ServerId) -> Unit) {
353 val request =
354 StartServerRequest.newBuilder().setName(TEST_SERVER_NAME).setUuid(TEST_UUID).build()
355 val response = mBumble.rfcommBlocking().startServer(request)
356
357 try {
358 block(response.server)
359 } finally {
360 mBumble
361 .rfcommBlocking()
362 .withDeadlineAfter(GRPC_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
363 .stopServer(
364 RfcommProto.StopServerRequest.newBuilder().setServer(response.server).build()
365 )
366 runBlocking { removeBondIfBonded(mBumbleDevice) }
367 }
368 }
369
disableHfpnull370 private fun disableHfp() =
371 runBlocking<Unit> {
372 val proxy = headsetFlow().first()
373 proxy.setConnectionPolicy(mBumbleDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)
374 }
375
headsetFlownull376 private suspend fun headsetFlow(): Flow<BluetoothHeadset> {
377 return callbackFlow {
378 val listener =
379 object : BluetoothProfile.ServiceListener {
380 lateinit var mBluetoothHeadset: BluetoothHeadset
381
382 override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
383 mBluetoothHeadset = proxy as BluetoothHeadset
384 trySend(mBluetoothHeadset)
385 }
386
387 override fun onServiceDisconnected(profile: Int) {}
388 }
389
390 mAdapter.getProfileProxy(mContext, listener, BluetoothProfile.HEADSET)
391
392 awaitClose {
393 mAdapter.closeProfileProxy(BluetoothProfile.HEADSET, listener.mBluetoothHeadset)
394 }
395 }
396 }
397
398 companion object {
399 private val TAG = RfcommTest::class.java.getSimpleName()
400 private val GRPC_TIMEOUT = Duration.ofSeconds(10)
401 private val BOND_TIMEOUT = Duration.ofSeconds(20)
402 private const val TEST_UUID = "00001101-0000-1000-8000-00805F9B34FB"
403 private const val TEST_SERVER_NAME = "RFCOMM Server"
404 }
405 }
406