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