Skip to content

Smoke

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/a2dp_test.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
class A2dpTest(navi_test_base.TwoDevicesTestBase):
  dut_supported_codecs: list[_A2dpCodec]

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if (
        self.dut.getprop(android_constants.Property.A2DP_SOURCE_ENABLED)
        != "true"
    ):
      raise signals.TestAbortClass("A2DP is not enabled on DUT.")
    if self.dut.device.build_info["hardware"] == "cutf_cvm":
      # Force enable OPUS on Cuttlefish.
      self.dut.setprop(_PROPERTY_OPUS_ENABLED, "true")
    self.dut_supported_codecs = [
        codec
        for codec in _A2dpCodec
        if int(
            self.dut.getprop(_PROPERTY_CODEC_PRIORITY % codec.name.lower())
            or "0"
        )
        > _VALUE_CODEC_DISABLED
        and (
            codec != _A2dpCodec.OPUS
            or self.dut.getprop(_PROPERTY_OPUS_ENABLED) == "true"
        )
    ]

  @override
  async def async_teardown_test(self) -> None:
    await super().async_teardown_test()
    self.dut.bt.audioStop()

  def _setup_a2dp_device(
      self, codecs: list[_A2dpCodec]
  ) -> tuple[avdtp.Listener, avrcp.Protocol]:
    """Sets up A2DP profile on REF.

    Args:
      codecs: A2DP codecs supported by REF.

    Returns:
      A tuple of (avdtp.Listener, avrcp.Protocol).
    """
    listener = a2dp_ext.setup_sink_server(
        self.ref.device,
        [codec.get_default_capabilities() for codec in codecs],
        _A2DP_SERVICE_RECORD_HANDLE,
    )
    avrcp_delegator = AvrcpDelegate(
        supported_events=(avrcp.EventId.VOLUME_CHANGED,)
    )
    avrcp_protocol = a2dp_ext.setup_avrcp_server(
        self.ref.device,
        avrcp_controller_handle=_AVRCP_CONTROLLER_RECORD_HANDLE,
        avrcp_target_handle=_AVRCP_TARGET_RECORD_HANDLE,
        delegate=avrcp_delegator,
    )

    return listener, avrcp_protocol

  async def _setup_a2dp_connection(self, ref_codecs: list[_A2dpCodec]) -> tuple[
      avrcp.Protocol,
      avdtp.Protocol,
  ]:
    """Sets up A2DP connection between DUT and REF.

    Args:
      ref_codecs: A2DP codecs supported by REF.

    Returns:
      A tuple of (avrcp.Protocol, avdtp.Protocol).
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
      ref_avdtp_listener, ref_avrcp_protocol = self._setup_a2dp_device(
          ref_codecs
      )
      ref_avdtp_connections = asyncio.Queue[avdtp.Protocol]()
      ref_avdtp_listener.on(
          ref_avdtp_listener.EVENT_CONNECTION, ref_avdtp_connections.put
      )

      self.logger.info("[DUT] Connect and pair REF.")
      ref_acl = await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for A2DP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Wait for A2DP connected."
      ):
        ref_avdtp_connection = await ref_avdtp_connections.get()
      self.logger.info("[DUT] Wait for A2DP becomes active.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      if ref_avrcp_protocol.avctp_protocol is not None:
        self.logger.info("[REF] AVRCP already connected.")
      else:
        self.logger.info("[REF] Connect AVRCP.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await ref_avrcp_protocol.connect(ref_acl)
        self.logger.info("[REF] AVRCP connected.")
    return ref_avrcp_protocol, ref_avdtp_connection

  async def _terminate_connection_from_ref(self) -> None:
    self.logger.info("[DUT] Terminate connection.")
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      ref_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          transport=bumble.core.PhysicalTransport.BR_EDR,
      )
      if ref_acl is None:
        self.logger.info("[REF] No ACL connection found.")
        return
      await ref_acl.disconnect()
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  async def _avrcp_key_click(
      self,
      ref_avrcp_protocol: avrcp.Protocol,
      key: avc.PassThroughFrame.OperationId,
  ) -> None:
    await ref_avrcp_protocol.send_key_event(key, pressed=True)
    await ref_avrcp_protocol.send_key_event(key, pressed=False)

  async def test_pair_and_connect(self) -> None:
    """Tests A2DP connection establishment right after a pairing session.

    Test steps:
      1. Setup A2DP on REF.
      2. Create bond from DUT.
      3. Wait A2DP connected on DUT (Android should autoconnect A2DP as AG).
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
      self._setup_a2dp_device([_A2dpCodec.SBC])

      self.logger.info("[DUT] Connect and pair REF.")
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for A2DP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      self.logger.info("[DUT] Wait for A2DP becomes active.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  async def test_paired_connect_outgoing(self) -> None:
    """Tests A2DP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from DUT.
      4. Wait A2DP connected on DUT.
      5. Disconnect from DUT.
      6. Wait A2DP disconnected on DUT.
    """
    await self.test_pair_and_connect()
    await self._terminate_connection_from_ref()

    with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
      self.logger.info("[DUT] Reconnect.")
      self.dut.bt.connect(self.ref.address)

      self.logger.info("[DUT] Wait for A2DP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      self.logger.info("[DUT] Wait for A2DP becomes active.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[DUT] Disconnect.")
      self.dut.bt.disconnect(self.ref.address)

      self.logger.info("[DUT] Wait for A2DP disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
      )

  async def test_paired_connect_incoming(self) -> None:
    """Tests A2DP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from REF.
      4. Wait A2DP connected on DUT.
      5. Disconnect from REF.
      6. Wait A2DP disconnected on DUT.
    """
    await self.test_pair_and_connect()
    await self._terminate_connection_from_ref()

    with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
      self.logger.info("[REF] Reconnect.")
      dut_ref_acl = await self.ref.device.connect(
          str(self.dut.address),
          bumble.core.BT_BR_EDR_TRANSPORT,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[REF] Authenticate and encrypt connection.")
      await dut_ref_acl.authenticate()
      await dut_ref_acl.encrypt()

      self.logger.info("[REF] Connect A2DP.")
      server = await avdtp.Protocol.connect(dut_ref_acl)
      server.add_sink(_A2dpCodec.AAC.get_default_capabilities())

      self.logger.info("[DUT] Wait for A2DP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      self.logger.info("[DUT] Wait for A2DP becomes active.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[REF] Disconnect.")
      await dut_ref_acl.disconnect()

      self.logger.info("[DUT] Wait for A2DP disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
      )

  @navi_test_base.parameterized(
      (_Issuer.DUT, [_A2dpCodec.SBC]),
      (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC]),
      (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.APTX]),
      (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.APTX_HD]),
      (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.LDAC]),
      (_Issuer.REF, [_A2dpCodec.SBC]),
      (_Issuer.REF, [_A2dpCodec.SBC, _A2dpCodec.AAC]),
  )
  @navi_test_base.retry(2)
  async def test_stream_start_and_stop(
      self,
      issuer: _Issuer,
      ref_codecs: list[_A2dpCodec],
  ) -> None:
    """Tests A2DP streaming controlled by the given issuer (DUT or REF).

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Start stream from the given issuer.
      3. Stop stream from DUT from the given issuer.

    Args:
      issuer: device to issue the volume change command.
      ref_codecs: A2DP codecs supported by REF.
    """
    # Select preferred codec and sink.
    for codec in _A2dpCodec:
      if codec in ref_codecs and codec in self.dut_supported_codecs:
        preferred_codec = codec
        break
    else:
      self.fail("No supported codec found on REF.")

    self.logger.info("Preferred codec: %s", preferred_codec.name)

    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)

    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb,
        self.dut.bl4a.register_callback(
            bl4a_api.Module.PLAYER
        ) as dut_player_cb,
    ):
      ref_avrcp_protocol, ref_avdtp_connection = (
          await self._setup_a2dp_connection(ref_codecs)
      )

      ref_sinks = a2dp_ext.find_local_endpoints_by_codec(
          ref_avdtp_connection,
          preferred_codec.codec_type,
          avdtp.LocalSink,
          vendor_id=preferred_codec.vendor_id,
          codec_id=preferred_codec.codec_id,
      )
      if not ref_sinks:
        self.fail("No sink found for codec %s." % preferred_codec.name)
      ref_sink = LocalSinkWrapper(ref_sinks[0])

      # If there is a playback, wait until it ends.
      if self.dut.bt.isA2dpPlaying(self.ref.address):
        self.logger.info("[DUT] A2DP is streaming, wait for A2DP stopped.")
        await dut_cb.wait_for_event(
            bl4a_api.A2dpPlayingStateChanged(
                self.ref.address, _A2dpState.NOT_PLAYING
            ),
        )
      async with (
          self.assert_not_timeout(
              _DEFAULT_STEP_TIMEOUT_SECONDS,
              msg="[REF] A2DP is streaming, wait for A2DP stopped.",
          ),
          ref_sink.condition,
      ):
        await ref_sink.condition.wait_for(
            lambda: ref_sink.stream_state != avdtp.AVDTP_STREAMING_STATE
        )

      # Register the sink buffer to receive the packets.
      buffer = a2dp_ext.register_sink_buffer(ref_sink.impl, preferred_codec)

      if issuer == _Issuer.DUT:
        self.logger.info("[DUT] Start stream.")
        self.dut.bt.audioPlaySine()
      else:
        self.logger.info("[REF] Start stream.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await self._avrcp_key_click(
              ref_avrcp_protocol, avc.PassThroughFrame.OperationId.PLAY
          )
        self.logger.info("[DUT] Wait for playback started.")
        await dut_player_cb.wait_for_event(
            bl4a_api.PlayerIsPlayingChanged(is_playing=True)
        )

      self.logger.info("[DUT] Wait for A2DP started.")
      await dut_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              address=self.ref.address, state=_A2dpState.PLAYING
          )
      )
      async with (
          self.assert_not_timeout(
              _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Wait for A2DP started."
          ),
          ref_sink.condition,
      ):
        await ref_sink.condition.wait_for(
            lambda: ref_sink.stream_state == avdtp.AVDTP_STREAMING_STATE
        )

      # Streaming for 1 second.
      await asyncio.sleep(1.0)

      if issuer == _Issuer.DUT:
        self.logger.info("[DUT] Stop stream.")
        self.dut.bt.audioPause()
      else:
        self.logger.info("[REF] Stop stream.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await self._avrcp_key_click(
              ref_avrcp_protocol, avc.PassThroughFrame.OperationId.PAUSE
          )
        self.logger.info("[DUT] Wait for playback stopped.")
        await dut_player_cb.wait_for_event(
            bl4a_api.PlayerIsPlayingChanged(is_playing=False)
        )

      self.logger.info("[DUT] Wait for A2DP stopped.")
      await dut_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              address=self.ref.address, state=_A2dpState.NOT_PLAYING
          )
      )
      async with (
          self.assert_not_timeout(
              _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Wait for A2DP stopped."
          ),
          ref_sink.condition,
      ):
        await ref_sink.condition.wait_for(
            lambda: ref_sink.stream_state != avdtp.AVDTP_STREAMING_STATE
        )
      if self.user_params.get(navi_test_base.RECORD_FULL_DATA) and buffer:
        self.write_test_output_data(
            f"a2dp_data.{preferred_codec.format}",
            buffer,
        )

      if (
          buffer is not None
          and preferred_codec != _A2dpCodec.LDAC
          and audio.SUPPORT_AUDIO_PROCESSING
      ):
        dominant_frequency = audio.get_dominant_frequency(
            buffer, format=preferred_codec.format
        )
        self.logger.info("Dominant frequency: %.2f", dominant_frequency)
        # Dominant frequency is not accurate on emulator.
        if not self.dut.device.is_emulator:
          self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

  @navi_test_base.parameterized(_Issuer.DUT, _Issuer.REF)
  async def test_set_absolute_volume(self, issuer: _Issuer) -> None:
    """Tests setting absolute volume.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Set absolute volume.

    Args:
      issuer: device to issue the volume change command.
    """
    ref_avrcp_protocol, _ = await self._setup_a2dp_connection([_A2dpCodec.SBC])
    ref_avrcp_delegator = ref_avrcp_protocol.delegate
    assert isinstance(ref_avrcp_delegator, AvrcpDelegate)

    dut_max_volume = self.dut.bt.getMaxVolume(_StreamType.MUSIC)
    dut_min_volume = self.dut.bt.getMinVolume(_StreamType.MUSIC)

    def android_to_avrcp_volume(volume: int) -> int:
      # Android JVM uses ROUND_HALF_UP policy, while Python uses ROUND_HALF_EVEN
      # by default, so we need to specify policy here.
      return int(
          decimal.Decimal(
              volume / dut_max_volume * _AVRCP_MAX_VOLUME
          ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
      )

    async with (
        self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS,
            msg="[REF] Wait for initial volume indicator.",
        ),
        ref_avrcp_delegator.condition,
    ):
      await ref_avrcp_delegator.condition.wait_for(
          lambda: (
              android_to_avrcp_volume(self.dut.bt.getVolume(_StreamType.MUSIC))
              == ref_avrcp_delegator.volume
          )
      )

    # DUT's VCS client might not be stable at the beginning. If we set volume
    # immediately, the volume might not be set correctly.
    await asyncio.sleep(_PREPARE_TIME_SECONDS)

    with self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb:
      for dut_expected_volume in range(dut_min_volume, dut_max_volume + 1):
        if self.dut.bt.getVolume(_StreamType.MUSIC) == dut_expected_volume:
          continue

        ref_expected_volume = android_to_avrcp_volume(dut_expected_volume)

        if issuer == _Issuer.DUT:
          self.logger.info("[DUT] Set volume to %d.", dut_expected_volume)
          self.dut.bt.setVolume(_StreamType.MUSIC, dut_expected_volume)
        else:
          self.logger.info("[REF] Set volume to %d.", ref_expected_volume)
          ref_avrcp_delegator.volume = ref_expected_volume
          ref_avrcp_protocol.notify_volume_changed(ref_expected_volume)

        self.logger.info("[DUT] Wait for volume changed.")
        volume_changed_event = await dut_audio_cb.wait_for_event(
            bl4a_api.VolumeChanged(
                stream_type=_StreamType.MUSIC, volume_value=matcher.ANY
            ),
        )
        self.assertEqual(volume_changed_event.volume_value, dut_expected_volume)

        # There won't be volume changed events on REF as issuer.
        if issuer == _Issuer.DUT:
          async with (
              self.assert_not_timeout(
                  _DEFAULT_STEP_TIMEOUT_SECONDS,
                  msg="[REF] Wait for volume changed.",
              ),
              ref_avrcp_delegator.condition,
          ):
            await ref_avrcp_delegator.condition.wait_for(
                lambda: ref_avrcp_delegator.volume == ref_expected_volume  # pylint: disable=cell-var-from-loop
            )

  @navi_test_base.retry(3)
  async def test_avrcp_previous_next_track(self) -> None:
    """Tests moving to previous and next track over AVRCP."""
    ref_avrcp_protocol, _ = await self._setup_a2dp_connection([_A2dpCodec.SBC])

    # Allow repeating to avoid the end of the track.
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    # Generate a sine wave audio file, and push it to DUT twice.
    with tempfile.NamedTemporaryFile(
        # On Windows, NamedTemporaryFile cannot be deleted if used multiple
        # times.
        delete=(sys.platform != "win32")
    ) as local_file:
      with wave.open(local_file.name, "wb") as wave_file:
        wave_file.setnchannels(1)
        wave_file.setsampwidth(2)
        wave_file.setframerate(48000)
        wave_file.writeframes(bytes(48000 * 2 * 5))  # 5 seconds.
      for i in range(2):
        self.dut.adb.push([
            local_file.name,
            f"/data/media/{self.dut.adb.current_user_id}/Music/sample-{i}.mp3",
        ])

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.PLAYER
    ) as dut_player_cb:
      # Play the first track.
      self.dut.bt.audioPlayFile("/storage/self/primary/Music/sample-0.mp3")
      # Add the second track to the player.
      self.dut.bt.addMediaItem("/storage/self/primary/Music/sample-1.mp3")

      self.logger.info("[DUT] Wait for playback started.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=True)
      )

      self.logger.info("[REF] Go to the next track.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await self._avrcp_key_click(
            ref_avrcp_protocol, avc.PassThroughFrame.OperationId.FORWARD
        )

      self.logger.info("[DUT] Wait for track transition.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerMediaItemTransition,
          lambda e: (e.uri is not None and "sample-1.mp3" in e.uri),
      )

      self.logger.info("[REF] Go back to the previous track.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await self._avrcp_key_click(
            ref_avrcp_protocol, avc.PassThroughFrame.OperationId.BACKWARD
        )

      self.logger.info("[DUT] Wait for track transition.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerMediaItemTransition,
          lambda e: (e.uri is not None and "sample-0.mp3" in e.uri),
      )

  async def test_noisy_handling(self) -> None:
    """Tests enabling noisy handling, and verify the player is paused after A2DP disconnected.

    Test steps:
      1. Enable noisy handling.
      2. Setup A2DP connection.
      3. Start streaming.
      4. Disconnect from REF.
      5. Wait for player paused.
    """
    if self.dut.device.is_emulator:
      self.skipTest("b/406208447 - Noisy handling is flaky on emulator.")

    # Enable audio noisy handling.
    self.dut.bt.setHandleAudioBecomingNoisy(True)

    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_a2dp_cb,
        self.dut.bl4a.register_callback(
            bl4a_api.Module.PLAYER
        ) as dut_player_cb,
    ):
      await self._setup_a2dp_connection([_A2dpCodec.SBC])
      self.logger.info("[DUT] Start stream.")
      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
      self.dut.bt.audioPlaySine()

      self.logger.info("[DUT] Wait for playback started.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=True),
      )
      if not self.dut.bt.isA2dpPlaying(self.ref.address):
        self.logger.info("[DUT] Wait for A2DP playing.")
        await dut_a2dp_cb.wait_for_event(
            bl4a_api.A2dpPlayingStateChanged(
                self.ref.address, _A2dpState.PLAYING
            ),
        )

    # Streaming for 1 second.
    await asyncio.sleep(1.0)

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.PLAYER
    ) as dut_player_cb:
      ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          transport=bumble.core.PhysicalTransport.BR_EDR,
      )
      if ref_dut_acl is None:
        self.fail("No ACL connection found?")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[REF] Disconnect.")
        await ref_dut_acl.disconnect()

      self.logger.info("[DUT] Wait for player paused.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=False),
      )

Tests moving to previous and next track over AVRCP.

Source code in navi/tests/smoke/a2dp_test.py
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
@navi_test_base.retry(3)
async def test_avrcp_previous_next_track(self) -> None:
  """Tests moving to previous and next track over AVRCP."""
  ref_avrcp_protocol, _ = await self._setup_a2dp_connection([_A2dpCodec.SBC])

  # Allow repeating to avoid the end of the track.
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
  # Generate a sine wave audio file, and push it to DUT twice.
  with tempfile.NamedTemporaryFile(
      # On Windows, NamedTemporaryFile cannot be deleted if used multiple
      # times.
      delete=(sys.platform != "win32")
  ) as local_file:
    with wave.open(local_file.name, "wb") as wave_file:
      wave_file.setnchannels(1)
      wave_file.setsampwidth(2)
      wave_file.setframerate(48000)
      wave_file.writeframes(bytes(48000 * 2 * 5))  # 5 seconds.
    for i in range(2):
      self.dut.adb.push([
          local_file.name,
          f"/data/media/{self.dut.adb.current_user_id}/Music/sample-{i}.mp3",
      ])

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.PLAYER
  ) as dut_player_cb:
    # Play the first track.
    self.dut.bt.audioPlayFile("/storage/self/primary/Music/sample-0.mp3")
    # Add the second track to the player.
    self.dut.bt.addMediaItem("/storage/self/primary/Music/sample-1.mp3")

    self.logger.info("[DUT] Wait for playback started.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=True)
    )

    self.logger.info("[REF] Go to the next track.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await self._avrcp_key_click(
          ref_avrcp_protocol, avc.PassThroughFrame.OperationId.FORWARD
      )

    self.logger.info("[DUT] Wait for track transition.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerMediaItemTransition,
        lambda e: (e.uri is not None and "sample-1.mp3" in e.uri),
    )

    self.logger.info("[REF] Go back to the previous track.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await self._avrcp_key_click(
          ref_avrcp_protocol, avc.PassThroughFrame.OperationId.BACKWARD
      )

    self.logger.info("[DUT] Wait for track transition.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerMediaItemTransition,
        lambda e: (e.uri is not None and "sample-0.mp3" in e.uri),
    )

Tests enabling noisy handling, and verify the player is paused after A2DP disconnected.

Test steps
  1. Enable noisy handling.
  2. Setup A2DP connection.
  3. Start streaming.
  4. Disconnect from REF.
  5. Wait for player paused.
Source code in navi/tests/smoke/a2dp_test.py
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
async def test_noisy_handling(self) -> None:
  """Tests enabling noisy handling, and verify the player is paused after A2DP disconnected.

  Test steps:
    1. Enable noisy handling.
    2. Setup A2DP connection.
    3. Start streaming.
    4. Disconnect from REF.
    5. Wait for player paused.
  """
  if self.dut.device.is_emulator:
    self.skipTest("b/406208447 - Noisy handling is flaky on emulator.")

  # Enable audio noisy handling.
  self.dut.bt.setHandleAudioBecomingNoisy(True)

  with (
      self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_a2dp_cb,
      self.dut.bl4a.register_callback(
          bl4a_api.Module.PLAYER
      ) as dut_player_cb,
  ):
    await self._setup_a2dp_connection([_A2dpCodec.SBC])
    self.logger.info("[DUT] Start stream.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
    self.dut.bt.audioPlaySine()

    self.logger.info("[DUT] Wait for playback started.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=True),
    )
    if not self.dut.bt.isA2dpPlaying(self.ref.address):
      self.logger.info("[DUT] Wait for A2DP playing.")
      await dut_a2dp_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              self.ref.address, _A2dpState.PLAYING
          ),
      )

  # Streaming for 1 second.
  await asyncio.sleep(1.0)

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.PLAYER
  ) as dut_player_cb:
    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address),
        transport=bumble.core.PhysicalTransport.BR_EDR,
    )
    if ref_dut_acl is None:
      self.fail("No ACL connection found?")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Disconnect.")
      await ref_dut_acl.disconnect()

    self.logger.info("[DUT] Wait for player paused.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=False),
    )

Tests A2DP connection establishment right after a pairing session.

Test steps
  1. Setup A2DP on REF.
  2. Create bond from DUT.
  3. Wait A2DP connected on DUT (Android should autoconnect A2DP as AG).
Source code in navi/tests/smoke/a2dp_test.py
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
async def test_pair_and_connect(self) -> None:
  """Tests A2DP connection establishment right after a pairing session.

  Test steps:
    1. Setup A2DP on REF.
    2. Create bond from DUT.
    3. Wait A2DP connected on DUT (Android should autoconnect A2DP as AG).
  """
  with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
    self._setup_a2dp_device([_A2dpCodec.SBC])

    self.logger.info("[DUT] Connect and pair REF.")
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for A2DP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )
    self.logger.info("[DUT] Wait for A2DP becomes active.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Tests A2DP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from REF.
  4. Wait A2DP connected on DUT.
  5. Disconnect from REF.
  6. Wait A2DP disconnected on DUT.
Source code in navi/tests/smoke/a2dp_test.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
async def test_paired_connect_incoming(self) -> None:
  """Tests A2DP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from REF.
    4. Wait A2DP connected on DUT.
    5. Disconnect from REF.
    6. Wait A2DP disconnected on DUT.
  """
  await self.test_pair_and_connect()
  await self._terminate_connection_from_ref()

  with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
    self.logger.info("[REF] Reconnect.")
    dut_ref_acl = await self.ref.device.connect(
        str(self.dut.address),
        bumble.core.BT_BR_EDR_TRANSPORT,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[REF] Authenticate and encrypt connection.")
    await dut_ref_acl.authenticate()
    await dut_ref_acl.encrypt()

    self.logger.info("[REF] Connect A2DP.")
    server = await avdtp.Protocol.connect(dut_ref_acl)
    server.add_sink(_A2dpCodec.AAC.get_default_capabilities())

    self.logger.info("[DUT] Wait for A2DP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )
    self.logger.info("[DUT] Wait for A2DP becomes active.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[REF] Disconnect.")
    await dut_ref_acl.disconnect()

    self.logger.info("[DUT] Wait for A2DP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
    )

Tests A2DP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from DUT.
  4. Wait A2DP connected on DUT.
  5. Disconnect from DUT.
  6. Wait A2DP disconnected on DUT.
Source code in navi/tests/smoke/a2dp_test.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
async def test_paired_connect_outgoing(self) -> None:
  """Tests A2DP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from DUT.
    4. Wait A2DP connected on DUT.
    5. Disconnect from DUT.
    6. Wait A2DP disconnected on DUT.
  """
  await self.test_pair_and_connect()
  await self._terminate_connection_from_ref()

  with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
    self.logger.info("[DUT] Reconnect.")
    self.dut.bt.connect(self.ref.address)

    self.logger.info("[DUT] Wait for A2DP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )
    self.logger.info("[DUT] Wait for A2DP becomes active.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[DUT] Disconnect.")
    self.dut.bt.disconnect(self.ref.address)

    self.logger.info("[DUT] Wait for A2DP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
    )

Tests setting absolute volume.

Test steps
  1. Setup pairing between DUT and REF.
  2. Set absolute volume.

Parameters:

Name Type Description Default
issuer _Issuer

device to issue the volume change command.

required
Source code in navi/tests/smoke/a2dp_test.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
@navi_test_base.parameterized(_Issuer.DUT, _Issuer.REF)
async def test_set_absolute_volume(self, issuer: _Issuer) -> None:
  """Tests setting absolute volume.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Set absolute volume.

  Args:
    issuer: device to issue the volume change command.
  """
  ref_avrcp_protocol, _ = await self._setup_a2dp_connection([_A2dpCodec.SBC])
  ref_avrcp_delegator = ref_avrcp_protocol.delegate
  assert isinstance(ref_avrcp_delegator, AvrcpDelegate)

  dut_max_volume = self.dut.bt.getMaxVolume(_StreamType.MUSIC)
  dut_min_volume = self.dut.bt.getMinVolume(_StreamType.MUSIC)

  def android_to_avrcp_volume(volume: int) -> int:
    # Android JVM uses ROUND_HALF_UP policy, while Python uses ROUND_HALF_EVEN
    # by default, so we need to specify policy here.
    return int(
        decimal.Decimal(
            volume / dut_max_volume * _AVRCP_MAX_VOLUME
        ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
    )

  async with (
      self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for initial volume indicator.",
      ),
      ref_avrcp_delegator.condition,
  ):
    await ref_avrcp_delegator.condition.wait_for(
        lambda: (
            android_to_avrcp_volume(self.dut.bt.getVolume(_StreamType.MUSIC))
            == ref_avrcp_delegator.volume
        )
    )

  # DUT's VCS client might not be stable at the beginning. If we set volume
  # immediately, the volume might not be set correctly.
  await asyncio.sleep(_PREPARE_TIME_SECONDS)

  with self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb:
    for dut_expected_volume in range(dut_min_volume, dut_max_volume + 1):
      if self.dut.bt.getVolume(_StreamType.MUSIC) == dut_expected_volume:
        continue

      ref_expected_volume = android_to_avrcp_volume(dut_expected_volume)

      if issuer == _Issuer.DUT:
        self.logger.info("[DUT] Set volume to %d.", dut_expected_volume)
        self.dut.bt.setVolume(_StreamType.MUSIC, dut_expected_volume)
      else:
        self.logger.info("[REF] Set volume to %d.", ref_expected_volume)
        ref_avrcp_delegator.volume = ref_expected_volume
        ref_avrcp_protocol.notify_volume_changed(ref_expected_volume)

      self.logger.info("[DUT] Wait for volume changed.")
      volume_changed_event = await dut_audio_cb.wait_for_event(
          bl4a_api.VolumeChanged(
              stream_type=_StreamType.MUSIC, volume_value=matcher.ANY
          ),
      )
      self.assertEqual(volume_changed_event.volume_value, dut_expected_volume)

      # There won't be volume changed events on REF as issuer.
      if issuer == _Issuer.DUT:
        async with (
            self.assert_not_timeout(
                _DEFAULT_STEP_TIMEOUT_SECONDS,
                msg="[REF] Wait for volume changed.",
            ),
            ref_avrcp_delegator.condition,
        ):
          await ref_avrcp_delegator.condition.wait_for(
              lambda: ref_avrcp_delegator.volume == ref_expected_volume  # pylint: disable=cell-var-from-loop
          )

Tests A2DP streaming controlled by the given issuer (DUT or REF).

Test steps
  1. Setup pairing between DUT and REF.
  2. Start stream from the given issuer.
  3. Stop stream from DUT from the given issuer.

Parameters:

Name Type Description Default
issuer _Issuer

device to issue the volume change command.

required
ref_codecs list[_A2dpCodec]

A2DP codecs supported by REF.

required
Source code in navi/tests/smoke/a2dp_test.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
@navi_test_base.parameterized(
    (_Issuer.DUT, [_A2dpCodec.SBC]),
    (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC]),
    (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.APTX]),
    (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.APTX_HD]),
    (_Issuer.DUT, [_A2dpCodec.SBC, _A2dpCodec.AAC, _A2dpCodec.LDAC]),
    (_Issuer.REF, [_A2dpCodec.SBC]),
    (_Issuer.REF, [_A2dpCodec.SBC, _A2dpCodec.AAC]),
)
@navi_test_base.retry(2)
async def test_stream_start_and_stop(
    self,
    issuer: _Issuer,
    ref_codecs: list[_A2dpCodec],
) -> None:
  """Tests A2DP streaming controlled by the given issuer (DUT or REF).

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Start stream from the given issuer.
    3. Stop stream from DUT from the given issuer.

  Args:
    issuer: device to issue the volume change command.
    ref_codecs: A2DP codecs supported by REF.
  """
  # Select preferred codec and sink.
  for codec in _A2dpCodec:
    if codec in ref_codecs and codec in self.dut_supported_codecs:
      preferred_codec = codec
      break
  else:
    self.fail("No supported codec found on REF.")

  self.logger.info("Preferred codec: %s", preferred_codec.name)

  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)

  with (
      self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb,
      self.dut.bl4a.register_callback(
          bl4a_api.Module.PLAYER
      ) as dut_player_cb,
  ):
    ref_avrcp_protocol, ref_avdtp_connection = (
        await self._setup_a2dp_connection(ref_codecs)
    )

    ref_sinks = a2dp_ext.find_local_endpoints_by_codec(
        ref_avdtp_connection,
        preferred_codec.codec_type,
        avdtp.LocalSink,
        vendor_id=preferred_codec.vendor_id,
        codec_id=preferred_codec.codec_id,
    )
    if not ref_sinks:
      self.fail("No sink found for codec %s." % preferred_codec.name)
    ref_sink = LocalSinkWrapper(ref_sinks[0])

    # If there is a playback, wait until it ends.
    if self.dut.bt.isA2dpPlaying(self.ref.address):
      self.logger.info("[DUT] A2DP is streaming, wait for A2DP stopped.")
      await dut_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              self.ref.address, _A2dpState.NOT_PLAYING
          ),
      )
    async with (
        self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS,
            msg="[REF] A2DP is streaming, wait for A2DP stopped.",
        ),
        ref_sink.condition,
    ):
      await ref_sink.condition.wait_for(
          lambda: ref_sink.stream_state != avdtp.AVDTP_STREAMING_STATE
      )

    # Register the sink buffer to receive the packets.
    buffer = a2dp_ext.register_sink_buffer(ref_sink.impl, preferred_codec)

    if issuer == _Issuer.DUT:
      self.logger.info("[DUT] Start stream.")
      self.dut.bt.audioPlaySine()
    else:
      self.logger.info("[REF] Start stream.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await self._avrcp_key_click(
            ref_avrcp_protocol, avc.PassThroughFrame.OperationId.PLAY
        )
      self.logger.info("[DUT] Wait for playback started.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=True)
      )

    self.logger.info("[DUT] Wait for A2DP started.")
    await dut_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.ref.address, state=_A2dpState.PLAYING
        )
    )
    async with (
        self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Wait for A2DP started."
        ),
        ref_sink.condition,
    ):
      await ref_sink.condition.wait_for(
          lambda: ref_sink.stream_state == avdtp.AVDTP_STREAMING_STATE
      )

    # Streaming for 1 second.
    await asyncio.sleep(1.0)

    if issuer == _Issuer.DUT:
      self.logger.info("[DUT] Stop stream.")
      self.dut.bt.audioPause()
    else:
      self.logger.info("[REF] Stop stream.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await self._avrcp_key_click(
            ref_avrcp_protocol, avc.PassThroughFrame.OperationId.PAUSE
        )
      self.logger.info("[DUT] Wait for playback stopped.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=False)
      )

    self.logger.info("[DUT] Wait for A2DP stopped.")
    await dut_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.ref.address, state=_A2dpState.NOT_PLAYING
        )
    )
    async with (
        self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Wait for A2DP stopped."
        ),
        ref_sink.condition,
    ):
      await ref_sink.condition.wait_for(
          lambda: ref_sink.stream_state != avdtp.AVDTP_STREAMING_STATE
      )
    if self.user_params.get(navi_test_base.RECORD_FULL_DATA) and buffer:
      self.write_test_output_data(
          f"a2dp_data.{preferred_codec.format}",
          buffer,
      )

    if (
        buffer is not None
        and preferred_codec != _A2dpCodec.LDAC
        and audio.SUPPORT_AUDIO_PROCESSING
    ):
      dominant_frequency = audio.get_dominant_frequency(
          buffer, format=preferred_codec.format
      )
      self.logger.info("Dominant frequency: %.2f", dominant_frequency)
      # Dominant frequency is not accurate on emulator.
      if not self.dut.device.is_emulator:
        self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/asha_test.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
class AshaTest(navi_test_base.TwoDevicesTestBase):
  ref_asha_service: asha.AshaService

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    # Force enabling ASHA Central on emulator.
    if self.dut.device.is_emulator:
      self.dut.setprop(_PROPERTY_ASHA_CENTRAL_ENABLED, "true")

    if self.dut.getprop(_PROPERTY_ASHA_CENTRAL_ENABLED) != "true":
      raise signals.TestAbortClass("ASHA Central is disabled.")

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self.ref_asha_service = asha.AshaService(
        capability=asha.DeviceCapabilities(0),
        hisyncid=secrets.token_bytes(8),
        device=self.ref.device,
    )
    self.ref.device.add_service(self.ref_asha_service)
    self.ref.device.advertising_data = (
        self.ref_asha_service.get_advertising_data()
    )

  async def _setup_paired_devices(self) -> None:
    with self.dut.bl4a.register_callback(bl4a_api.Module.ASHA) as dut_cb:
      await self.le_connect_and_pair(
          ref_address_type=hci.OwnAddressType.RANDOM, connect_profiles=True
      )

      self.logger.info("[DUT] Wait for ASHA connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
      )

  async def test_connect(self) -> None:
    """Tests ASHA connection.

    Test steps:
      1. Pair with REF.
      2. Verify ASHA is connected.
    """
    await self._setup_paired_devices()
    self.assertIn(
        self.ref.random_address,
        self.dut.bt.getActiveDevices(android_constants.Profile.HEARING_AID),
    )

  async def test_reconnect(self) -> None:
    """Tests ASHA reconnection.

    Test steps:
      1. Pair with REF.
      2. Verify ASHA is connected.
      3. Disconnect from REF.
      4. Restart advertising on REF.
      5. Verify ASHA is connected.
    """
    await self._setup_paired_devices()

    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address)
    )
    if ref_dut_acl is None:
      self.fail("No ACL connection found.")
    self.logger.info("[REF] Disconnect")
    await ref_dut_acl.disconnect()

    with self.dut.bl4a.register_callback(bl4a_api.Module.ASHA) as dut_cb:
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        self.logger.info("[REF] Start advertising")
        await self.ref.device.create_advertising_set(
            advertising_data=self.ref_asha_service.get_advertising_data(),
            advertising_parameters=bumble_device.AdvertisingParameters(
                own_address_type=hci.OwnAddressType.RANDOM,
                primary_advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
                primary_advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
            ),
            auto_restart=False,
        )

      self.logger.info("[DUT] Wait for ASHA connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
      )

  @navi_test_base.parameterized(
      bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION,
      bl4a_api.AudioAttributes.Usage.MEDIA,
  )
  async def test_streaming(self, usage: bl4a_api.AudioAttributes.Usage) -> None:
    """Tests ASHA streaming.

    Test steps:
      1. Establish ASHA connection.
      2. (Optional) Start phone call.
      3. Start streaming.
      4. Verify audio data is received.
      5. Stop streaming.

    Args:
      usage: The type of stream to test.
    """
    await self._setup_paired_devices()

    sink_buffer = bytearray()
    self.ref_asha_service.audio_sink = sink_buffer.extend
    watcher = pyee_extensions.EventWatcher()
    start_events = watcher.async_monitor(
        self.ref_asha_service, asha.AshaService.Event.STARTED
    )
    stop_events = watcher.async_monitor(
        self.ref_asha_service, asha.AshaService.Event.STOPPED
    )

    with contextlib.ExitStack() as exit_stack:
      if usage == bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION:
        self.logger.info("[DUT] Start phone call")
        exit_stack.enter_context(
            self.dut.bl4a.make_phone_call(
                caller_name="Pixel Bluetooth",
                caller_number="123456789",
                direction=constants.Direction.OUTGOING,
            )
        )
      self.dut.bl4a.set_audio_attributes(
          bl4a_api.AudioAttributes(usage=usage), handle_audio_focus=False
      )

      self.logger.info("[DUT] Start streaming")
      await asyncio.to_thread(self.dut.bt.audioPlaySine)
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        self.logger.info("[REF] Wait for audio started")
        await start_events.get()

      await asyncio.sleep(_STREAMING_TIME_SECONDS)

      self.logger.info("[DUT] Stop streaming")
      await asyncio.to_thread(self.dut.bt.audioStop)
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        self.logger.info("[REF] Wait for audio stopped")
        await stop_events.get()

      if (
          self.user_params.get(navi_test_base.RECORD_FULL_DATA)
          and sink_buffer
      ):
        self.write_test_output_data(
            "asha_data.g722",
            sink_buffer,
        )

      if audio.SUPPORT_AUDIO_PROCESSING:
        dominant_frequency = audio.get_dominant_frequency(
            sink_buffer, format="g722"
        )
        self.logger.info("Dominant frequency: %.2f", dominant_frequency)
        # Dominant frequency is not accurate on emulator.
        if not self.dut.device.is_emulator:
          self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

  async def test_set_volume(self) -> None:
    """Tests ASHA set volume.

    Test steps:
      1. Establish ASHA connection.
      2. Set volume to min.
      3. Verify volume changed to -128.
      4. Set volume to max.
      5. Verify volume changed to 0.
    """
    await self._setup_paired_devices()

    stream_type = android_constants.StreamType.MUSIC
    volumes = pyee_extensions.EventTriggeredValueObserver(
        self.ref_asha_service,
        asha.AshaService.Event.VOLUME_CHANGED,
        lambda: self.ref_asha_service.volume,
    )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[DUT] Set volume to min")
      self.dut.bt.setVolume(stream_type, self.dut.bt.getMinVolume(stream_type))
      self.logger.info("[REF] Wait for volume changed")
      await volumes.wait_for_target_value(-128)

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[DUT] Set volume to max")
      self.dut.bt.setVolume(stream_type, self.dut.bt.getMaxVolume(stream_type))
      self.logger.info("[REF] Wait for volume changed")
      await volumes.wait_for_target_value(0)

Tests ASHA connection.

Test steps
  1. Pair with REF.
  2. Verify ASHA is connected.
Source code in navi/tests/smoke/asha_test.py
79
80
81
82
83
84
85
86
87
88
89
90
async def test_connect(self) -> None:
  """Tests ASHA connection.

  Test steps:
    1. Pair with REF.
    2. Verify ASHA is connected.
  """
  await self._setup_paired_devices()
  self.assertIn(
      self.ref.random_address,
      self.dut.bt.getActiveDevices(android_constants.Profile.HEARING_AID),
  )

Tests ASHA reconnection.

Test steps
  1. Pair with REF.
  2. Verify ASHA is connected.
  3. Disconnect from REF.
  4. Restart advertising on REF.
  5. Verify ASHA is connected.
Source code in navi/tests/smoke/asha_test.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
async def test_reconnect(self) -> None:
  """Tests ASHA reconnection.

  Test steps:
    1. Pair with REF.
    2. Verify ASHA is connected.
    3. Disconnect from REF.
    4. Restart advertising on REF.
    5. Verify ASHA is connected.
  """
  await self._setup_paired_devices()

  ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
      hci.Address(self.dut.address)
  )
  if ref_dut_acl is None:
    self.fail("No ACL connection found.")
  self.logger.info("[REF] Disconnect")
  await ref_dut_acl.disconnect()

  with self.dut.bl4a.register_callback(bl4a_api.Module.ASHA) as dut_cb:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Start advertising")
      await self.ref.device.create_advertising_set(
          advertising_data=self.ref_asha_service.get_advertising_data(),
          advertising_parameters=bumble_device.AdvertisingParameters(
              own_address_type=hci.OwnAddressType.RANDOM,
              primary_advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
              primary_advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
          ),
          auto_restart=False,
      )

    self.logger.info("[DUT] Wait for ASHA connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
    )

Tests ASHA set volume.

Test steps
  1. Establish ASHA connection.
  2. Set volume to min.
  3. Verify volume changed to -128.
  4. Set volume to max.
  5. Verify volume changed to 0.
Source code in navi/tests/smoke/asha_test.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
async def test_set_volume(self) -> None:
  """Tests ASHA set volume.

  Test steps:
    1. Establish ASHA connection.
    2. Set volume to min.
    3. Verify volume changed to -128.
    4. Set volume to max.
    5. Verify volume changed to 0.
  """
  await self._setup_paired_devices()

  stream_type = android_constants.StreamType.MUSIC
  volumes = pyee_extensions.EventTriggeredValueObserver(
      self.ref_asha_service,
      asha.AshaService.Event.VOLUME_CHANGED,
      lambda: self.ref_asha_service.volume,
  )

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[DUT] Set volume to min")
    self.dut.bt.setVolume(stream_type, self.dut.bt.getMinVolume(stream_type))
    self.logger.info("[REF] Wait for volume changed")
    await volumes.wait_for_target_value(-128)

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[DUT] Set volume to max")
    self.dut.bt.setVolume(stream_type, self.dut.bt.getMaxVolume(stream_type))
    self.logger.info("[REF] Wait for volume changed")
    await volumes.wait_for_target_value(0)

Tests ASHA streaming.

Test steps
  1. Establish ASHA connection.
  2. (Optional) Start phone call.
  3. Start streaming.
  4. Verify audio data is received.
  5. Stop streaming.

Parameters:

Name Type Description Default
usage Usage

The type of stream to test.

required
Source code in navi/tests/smoke/asha_test.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@navi_test_base.parameterized(
    bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION,
    bl4a_api.AudioAttributes.Usage.MEDIA,
)
async def test_streaming(self, usage: bl4a_api.AudioAttributes.Usage) -> None:
  """Tests ASHA streaming.

  Test steps:
    1. Establish ASHA connection.
    2. (Optional) Start phone call.
    3. Start streaming.
    4. Verify audio data is received.
    5. Stop streaming.

  Args:
    usage: The type of stream to test.
  """
  await self._setup_paired_devices()

  sink_buffer = bytearray()
  self.ref_asha_service.audio_sink = sink_buffer.extend
  watcher = pyee_extensions.EventWatcher()
  start_events = watcher.async_monitor(
      self.ref_asha_service, asha.AshaService.Event.STARTED
  )
  stop_events = watcher.async_monitor(
      self.ref_asha_service, asha.AshaService.Event.STOPPED
  )

  with contextlib.ExitStack() as exit_stack:
    if usage == bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION:
      self.logger.info("[DUT] Start phone call")
      exit_stack.enter_context(
          self.dut.bl4a.make_phone_call(
              caller_name="Pixel Bluetooth",
              caller_number="123456789",
              direction=constants.Direction.OUTGOING,
          )
      )
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(usage=usage), handle_audio_focus=False
    )

    self.logger.info("[DUT] Start streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for audio started")
      await start_events.get()

    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info("[DUT] Stop streaming")
    await asyncio.to_thread(self.dut.bt.audioStop)
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for audio stopped")
      await stop_events.get()

    if (
        self.user_params.get(navi_test_base.RECORD_FULL_DATA)
        and sink_buffer
    ):
      self.write_test_output_data(
          "asha_data.g722",
          sink_buffer,
      )

    if audio.SUPPORT_AUDIO_PROCESSING:
      dominant_frequency = audio.get_dominant_frequency(
          sink_buffer, format="g722"
      )
      self.logger.info("Dominant frequency: %.2f", dominant_frequency)
      # Dominant frequency is not accurate on emulator.
      if not self.dut.device.is_emulator:
        self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/bluetooth_service_test.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class BluetoothServiceTest(navi_test_base.TwoDevicesTestBase):

  @navi_test_base.named_parameterized(
      ("ble_scan_enabled", 1, android_constants.AdapterState.BLE_ON),
      ("ble_scan_disabled", 0, android_constants.AdapterState.OFF),
  )
  async def test_bt_disabled(
      self, scan_mode: int, target_state: android_constants.AdapterState
  ) -> None:
    """Tests adapter state after BT off.

    Test steps:
      1. Enable/Disable ble_scan_always_enabled.
      2. Disable bluetooth on DUT.
      3. Check if bluetooth state is ble_on/off.

    Args:
      scan_mode: Enable/Disable ble_scan_always_enabled.
      target_state: Expected adapter state after BT off.
    """

    current_mode = self.dut.shell(
        ["settings get global ble_scan_always_enabled"]
    )
    self.test_case_context.callback(
        lambda: self.dut.shell(
            f"settings put global ble_scan_always_enabled {current_mode}"
        )
    )

    self.logger.info("[DUT] Set ble_scan_always_enabled to %s", scan_mode)
    self.dut.shell(
        ["settings put global ble_scan_always_enabled", str(scan_mode)]
    )

    self.logger.info("[DUT] Disable bluetooth.")
    self.assertTrue(self.dut.bt.disable())

    self.logger.info("[DUT] Check adapter state is at %s.", target_state.name)
    self.dut.bt.waitForAdapterState(target_state)

  @navi_test_base.named_parameterized(
      ("ble_scan_enabled", 1, android_constants.AdapterState.BLE_ON),
      ("ble_scan_disabled", 0, android_constants.AdapterState.OFF),
  )
  async def test_no_connection_after_bt_disabled(
      self, scan_mode: int, target_state: android_constants.AdapterState
  ) -> None:
    """Tests no connection after BT off.

    Test steps:
      1. Setup A2DP connection between DUT and REF.
      2. Connect and pair DUT and REF.
      3. Enable/Disable ble_scan_always_enabled.
      4. Disable bluetooth on DUT.
      5. Check if acl is disconnected.

    Args:
      scan_mode: Enable/Disable ble_scan_always_enabled.
      target_state: Expected adapter state after BT off.
    """
    a2dp_ext.setup_sink_server(
        self.ref.device,
        [_A2dpCodec.SBC.get_default_capabilities()],
        _A2DP_SERVICE_RECORD_HANDLE,
    )

    with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
      connection = await self.classic_connect_and_pair()
      self.logger.info("[DUT] Connection: %s", connection)

      disconnection = asyncio.Queue[int]()
      connection.on(connection.EVENT_DISCONNECTION, disconnection.put_nowait)

      self.logger.info("[DUT] Wait for A2DP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

      await self.test_bt_disabled(scan_mode, target_state)

      self.logger.info("[DUT] Wait for A2DP disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
      )

      async with self.assert_not_timeout(
          _DEFAULT_DISCONNECTION_TIMEOUT_SECONDS,
          msg="[REF] Wait for acl disconnection.",
      ):
        await disconnection.get()

Tests adapter state after BT off.

Test steps
  1. Enable/Disable ble_scan_always_enabled.
  2. Disable bluetooth on DUT.
  3. Check if bluetooth state is ble_on/off.

Parameters:

Name Type Description Default
scan_mode int

Enable/Disable ble_scan_always_enabled.

required
target_state AdapterState

Expected adapter state after BT off.

required
Source code in navi/tests/smoke/bluetooth_service_test.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@navi_test_base.named_parameterized(
    ("ble_scan_enabled", 1, android_constants.AdapterState.BLE_ON),
    ("ble_scan_disabled", 0, android_constants.AdapterState.OFF),
)
async def test_bt_disabled(
    self, scan_mode: int, target_state: android_constants.AdapterState
) -> None:
  """Tests adapter state after BT off.

  Test steps:
    1. Enable/Disable ble_scan_always_enabled.
    2. Disable bluetooth on DUT.
    3. Check if bluetooth state is ble_on/off.

  Args:
    scan_mode: Enable/Disable ble_scan_always_enabled.
    target_state: Expected adapter state after BT off.
  """

  current_mode = self.dut.shell(
      ["settings get global ble_scan_always_enabled"]
  )
  self.test_case_context.callback(
      lambda: self.dut.shell(
          f"settings put global ble_scan_always_enabled {current_mode}"
      )
  )

  self.logger.info("[DUT] Set ble_scan_always_enabled to %s", scan_mode)
  self.dut.shell(
      ["settings put global ble_scan_always_enabled", str(scan_mode)]
  )

  self.logger.info("[DUT] Disable bluetooth.")
  self.assertTrue(self.dut.bt.disable())

  self.logger.info("[DUT] Check adapter state is at %s.", target_state.name)
  self.dut.bt.waitForAdapterState(target_state)

Tests no connection after BT off.

Test steps
  1. Setup A2DP connection between DUT and REF.
  2. Connect and pair DUT and REF.
  3. Enable/Disable ble_scan_always_enabled.
  4. Disable bluetooth on DUT.
  5. Check if acl is disconnected.

Parameters:

Name Type Description Default
scan_mode int

Enable/Disable ble_scan_always_enabled.

required
target_state AdapterState

Expected adapter state after BT off.

required
Source code in navi/tests/smoke/bluetooth_service_test.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@navi_test_base.named_parameterized(
    ("ble_scan_enabled", 1, android_constants.AdapterState.BLE_ON),
    ("ble_scan_disabled", 0, android_constants.AdapterState.OFF),
)
async def test_no_connection_after_bt_disabled(
    self, scan_mode: int, target_state: android_constants.AdapterState
) -> None:
  """Tests no connection after BT off.

  Test steps:
    1. Setup A2DP connection between DUT and REF.
    2. Connect and pair DUT and REF.
    3. Enable/Disable ble_scan_always_enabled.
    4. Disable bluetooth on DUT.
    5. Check if acl is disconnected.

  Args:
    scan_mode: Enable/Disable ble_scan_always_enabled.
    target_state: Expected adapter state after BT off.
  """
  a2dp_ext.setup_sink_server(
      self.ref.device,
      [_A2dpCodec.SBC.get_default_capabilities()],
      _A2DP_SERVICE_RECORD_HANDLE,
  )

  with self.dut.bl4a.register_callback(bl4a_api.Module.A2DP) as dut_cb:
    connection = await self.classic_connect_and_pair()
    self.logger.info("[DUT] Connection: %s", connection)

    disconnection = asyncio.Queue[int]()
    connection.on(connection.EVENT_DISCONNECTION, disconnection.put_nowait)

    self.logger.info("[DUT] Wait for A2DP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    await self.test_bt_disabled(scan_mode, target_state)

    self.logger.info("[DUT] Wait for A2DP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
    )

    async with self.assert_not_timeout(
        _DEFAULT_DISCONNECTION_TIMEOUT_SECONDS,
        msg="[REF] Wait for acl disconnection.",
    ):
      await disconnection.get()

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/broadcast_test.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
class BroadcastTest(navi_test_base.TwoDevicesTestBase):
  _broadcast_enabled: bool = False
  _bass_enabled: bool = False
  _ref_bass_service: _BroadcastAudioScanService

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    if self.dut.getprop(_PROPERTY_BROADCAST_SOURCE_ENABLED) == "true":
      self._broadcast_enabled = True
    if self.dut.getprop(_PROPERTY_BROADCAST_ASSIST_ENABLED) == "true":
      self._bass_enabled = True

    if not self._broadcast_enabled and not self._bass_enabled:
      raise signals.TestAbortClass("Broadcast source and BASS are not enabled.")

    if not self.ref.device.supports_le_features(
        hci.LeFeatureMask.PERIODIC_ADVERTISING_SYNC_TRANSFER_RECIPIENT
        | hci.LeFeatureMask.PERIODIC_ADVERTISING_SYNC_TRANSFER_SENDER
        | hci.LeFeatureMask.LE_PERIODIC_ADVERTISING
        | hci.LeFeatureMask.ISOCHRONOUS_BROADCASTER
    ):
      raise signals.TestAbortClass("REF does not support LE features.")

    self.ref.config.cis_enabled = True
    # Disable the allow list to allow the connect LE Audio to Bumble.
    self.dut.setprop(_PROPERTY_BYPASS_ALLOW_LIST, "true")

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self._ref_bass_service = _BroadcastAudioScanService()

  @override
  async def async_teardown_test(self) -> None:
    await super().async_teardown_test()
    self.dut.bt.audioStop()

  async def _receive_broadcast_on_ref(
      self,
      broadcast_id: int,
      subgroup_index: int,
      broadcast_code: bytes | None = None,
  ) -> tuple[device.BigSync, bap.BasicAudioAnnouncement]:
    broadcast_advertisements = asyncio.Queue[device.Advertisement]()

    def on_advertisement(advertisement: device.Advertisement) -> None:
      if (
          (
              ads := advertisement.data.get_all(
                  core.AdvertisingData.Type.SERVICE_DATA_16_BIT_UUID
              )
          )
          and (
              broadcast_audio_announcement := next(
                  (
                      ad[1]
                      for ad in ads
                      if isinstance(ad, tuple)
                      and ad[0]
                      == gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
                  ),
                  None,
              )
          )
          and (
              bap.BroadcastAudioAnnouncement.from_bytes(
                  broadcast_audio_announcement
              ).broadcast_id
              == broadcast_id
          )
      ):
        broadcast_advertisements.put_nowait(advertisement)

    self.ref.device.on(self.ref.device.EVENT_ADVERTISEMENT, on_advertisement)

    self.logger.info("[REF] Start scanning")
    await self.ref.device.start_scanning()

    self.logger.info("[REF] Wait for broadcast advertisement")
    broadcast_advertisement = await broadcast_advertisements.get()

    self.logger.info("[REF] Create periodic advertising sync")
    pa_sync = await self.ref.device.create_periodic_advertising_sync(
        advertiser_address=broadcast_advertisement.address,
        sid=broadcast_advertisement.sid,
    )

    basic_audio_announcements = asyncio.Queue[bap.BasicAudioAnnouncement]()
    big_info_advertisements = asyncio.Queue[device.BigInfoAdvertisement]()

    def on_periodic_advertisement(
        advertisement: device.PeriodicAdvertisement,
    ) -> None:
      if (advertising_data := advertisement.data) is None:  # type: ignore[attribute-error]
        return

      for service_data in advertising_data.get_all(
          core.AdvertisingData.SERVICE_DATA
      ):
        service_uuid, data = cast(tuple[core.UUID, bytes], service_data)

        if service_uuid == gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE:
          basic_audio_announcements.put_nowait(
              bap.BasicAudioAnnouncement.from_bytes(data)
          )
          break

    def on_biginfo_advertisement(
        advertisement: device.BigInfoAdvertisement,
    ) -> None:
      big_info_advertisements.put_nowait(advertisement)

    pa_sync.on(pa_sync.EVENT_PERIODIC_ADVERTISEMENT, on_periodic_advertisement)
    pa_sync.on(pa_sync.EVENT_BIGINFO_ADVERTISEMENT, on_biginfo_advertisement)

    self.logger.info("[REF] Wait for basic audio announcement")
    basic_audio_announcement = await basic_audio_announcements.get()
    subgroup = basic_audio_announcement.subgroups[subgroup_index]

    self.logger.info("[REF] Wait for BIG info advertisement")
    await big_info_advertisements.get()

    self.logger.info("[REF] Stop scanning")
    await self.ref.device.stop_scanning()

    self.logger.info("[REF] Sync with BIG")
    big_sync = await self.ref.device.create_big_sync(
        pa_sync,
        device.BigSyncParameters(
            big_sync_timeout=0x4000,
            bis=[bis.index for bis in subgroup.bis],
            broadcast_code=broadcast_code,
        ),
    )
    return big_sync, basic_audio_announcement

  async def _start_broadcast_on_ref(
      self,
      broadcast_id: int,
      broadcast_name: str,
      broadcast_code: bytes | None,
      sampling_frequency: bap.SamplingFrequency,
      frame_duration: bap.FrameDuration,
      octets_per_codec_frame: int,
  ) -> tuple[device.AdvertisingSet, device.Big]:
    broadcast_audio_announcement = bap.BroadcastAudioAnnouncement(broadcast_id)

    self.logger.info("[REF] Start Advertising")
    advertising_set = await self.ref.device.create_advertising_set(
        advertising_parameters=device.AdvertisingParameters(
            advertising_event_properties=device.AdvertisingEventProperties(
                is_connectable=False
            ),
            primary_advertising_interval_min=100,
            primary_advertising_interval_max=200,
        ),
        advertising_data=(
            broadcast_audio_announcement.get_advertising_data()
            + bytes(
                core.AdvertisingData([(
                    core.AdvertisingData.BROADCAST_NAME,
                    broadcast_name.encode("utf-8"),
                )])
            )
        ),
        periodic_advertising_parameters=device.PeriodicAdvertisingParameters(
            periodic_advertising_interval_min=80,
            periodic_advertising_interval_max=160,
        ),
        periodic_advertising_data=_make_basic_audio_announcement(
            sampling_frequency=sampling_frequency,
            frame_duration=frame_duration,
            octets_per_codec_frame=octets_per_codec_frame,
        ).get_advertising_data(),
        auto_restart=True,
        auto_start=True,
    )

    self.logger.info("[REF] Start Periodic Advertising")
    await advertising_set.start_periodic()

    self.logger.info("[REF] Create BIG")
    big = await self.ref.device.create_big(
        advertising_set,
        parameters=device.BigParameters(
            num_bis=2,
            sdu_interval=10000,
            max_sdu=100,
            max_transport_latency=65,
            rtn=4,
            broadcast_code=broadcast_code,
        ),
    )

    return advertising_set, big

  async def _prepare_paired_devices(self) -> None:
    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_lea_cb,
        self.dut.bl4a.register_callback(bl4a_api.Module.BASS) as dut_bass_cb,
    ):
      # TODO: Currently, there must be another audio device to
      # enable broadcast, so we add a PACS and ASCS to make the Bumble device
      # look like an LEHS.
      self.ref.device.add_service(
          pacs.PublishedAudioCapabilitiesService(
              supported_source_context=bap.ContextType(0x0000),
              available_source_context=bap.ContextType(0x0000),
              supported_sink_context=bap.ContextType(0xFFFF),
              available_sink_context=bap.ContextType(0xFFFF),
              sink_audio_locations=(
                  bap.AudioLocation.FRONT_LEFT | bap.AudioLocation.FRONT_RIGHT
              ),
              sink_pac=[
                  pacs.PacRecord(
                      coding_format=hci.CodingFormat(hci.CodecID.LC3),
                      codec_specific_capabilities=bap.CodecSpecificCapabilities(
                          supported_sampling_frequencies=(
                              bap.SupportedSamplingFrequency.FREQ_16000
                              | bap.SupportedSamplingFrequency.FREQ_32000
                              | bap.SupportedSamplingFrequency.FREQ_48000
                          ),
                          supported_frame_durations=(
                              bap.SupportedFrameDuration.DURATION_10000_US_SUPPORTED
                          ),
                          supported_audio_channel_count=[1, 2],
                          min_octets_per_codec_frame=26,
                          max_octets_per_codec_frame=240,
                          supported_max_codec_frames_per_sdu=2,
                      ),
                  )
              ],
          )
      )
      self.ref.device.add_service(
          ascs.AudioStreamControlService(self.ref.device, sink_ase_id=[1])
      )
      self.ref.device.add_service(self._ref_bass_service)
      await self.le_connect_and_pair(
          hci.OwnAddressType.RANDOM, connect_profiles=True
      )
      self.logger.info("[DUT] Wait for LE Audio connected")
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      self.logger.info("[DUT] Wait for BASS connected")
      await dut_bass_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

  @navi_test_base.named_parameterized(
      standard_quality=dict(
          quality=android_constants.LeAudioBroadcastQuality.STANDARD,
          broadcast_code=None,
          expected_sampling_frequency=bap.SamplingFrequency.FREQ_24000,
      ),
      high_quality=dict(
          quality=android_constants.LeAudioBroadcastQuality.HIGH,
          broadcast_code=None,
          expected_sampling_frequency=bap.SamplingFrequency.FREQ_48000,
      ),
      high_quality_encrypted=dict(
          quality=android_constants.LeAudioBroadcastQuality.HIGH,
          broadcast_code=b"1234567890abcdef",
          expected_sampling_frequency=bap.SamplingFrequency.FREQ_48000,
      ),
  )
  async def test_broadcast_start_stop(
      self,
      quality: android_constants.LeAudioBroadcastQuality,
      broadcast_code: bytes | None,
      expected_sampling_frequency: bap.SamplingFrequency,
  ) -> None:
    """Tests broadcasting on DUT, and receiving on REF.

    Test steps:
      1. Start broadcasting on DUT.
      2. Wait for broadcast advertisement on REF.
      3. Create periodic advertising sync on REF.
      4. Wait for basic audio announcement on REF.
      5. Wait for BIG info advertisement on REF.
      6. Create BIG sync on REF.
      7. Stop broadcasting on DUT.
      8. Wait for BIG sync lost on REF.

    Args:
      quality: The quality of the broadcast.
      broadcast_code: The broadcast code of the broadcast.
      expected_sampling_frequency: The expected sampling frequency of the
        broadcast.
    """
    if not self._broadcast_enabled:
      self.skipTest("Broadcast source is not enabled.")

    # TODO: LEHS must be connected before starting broadcast.
    await self._prepare_paired_devices()

    self.logger.info("[DUT] Start broadcasting")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      broadcast = await self.dut.bl4a.start_le_audio_broadcast(
          broadcast_code=broadcast_code,
          subgroups=[
              bl4a_api.LeAudioBroadcastSubgroupSettings(quality=quality)
          ],
      )

    self.logger.info("[DUT] Broadcast created, ID: %d", broadcast.broadcast_id)

    # Since 25Q1, broadcast will be started only during audio playback.
    self.logger.info("[DUT] Start audio playback")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
    self.dut.bt.audioPlaySine()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      big_sync, basic_audio_announcement = await self._receive_broadcast_on_ref(
          broadcast_id=broadcast.broadcast_id,
          subgroup_index=_SUBGROUP_INDEX,
          broadcast_code=broadcast_code,
      )

    self.assertEqual(
        basic_audio_announcement.subgroups[
            _SUBGROUP_INDEX
        ].codec_specific_configuration.sampling_frequency,
        expected_sampling_frequency,
    )

    sync_lost = asyncio.Event()
    big_sync.once(big_sync.Event.TERMINATION, lambda _: sync_lost.set())

    self.logger.info("[DUT] Stop broadcasting")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      await broadcast.stop()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Wait for sync lost")
      await sync_lost.wait()

  @navi_test_base.named_parameterized(
      dict(
          testcase_name="unencrypted",
          broadcast_code=None,
      ),
      dict(
          testcase_name="encrypted",
          broadcast_code=b"1234567890abcdef",
      ),
  )
  async def test_broadcast_assist_search(
      self, broadcast_code: bytes | None
  ) -> None:
    """Tests broadcasting on REF, and search from DUT.

    Test steps:
      1. Start broadcasting on REF.
      2. Search for broadcast on DUT.
      3. Verify the broadcast source found event.

    Args:
      broadcast_code: The broadcast code of the broadcast.
    """
    if not self._bass_enabled:
      self.skipTest("BASS is not enabled.")

    broadcast_id = random.randint(0, 0xFFFFFF)
    broadcast_name = "Bumble Auracast"
    self.logger.info("[REF] Broadcast ID: %d", broadcast_id)

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      await self._start_broadcast_on_ref(
          broadcast_id=broadcast_id,
          broadcast_name=broadcast_name,
          broadcast_code=broadcast_code,
          sampling_frequency=bap.SamplingFrequency.FREQ_48000,
          frame_duration=bap.FrameDuration.DURATION_10000_US,
          octets_per_codec_frame=100,
      )

    with self.dut.bl4a.register_callback(bl4a_api.Module.BASS) as bass_callback:
      self.dut.bt.bassStartSearching()
      self.logger.info("[DUT] Wait for broadcast source found")
      source_found_event = await bass_callback.wait_for_event(
          bl4a_api.BroadcastSourceFound,
          lambda e: (
              cast(bl4a_api.BroadcastSourceFound, e).source.broadcast_id
              == broadcast_id
          ),
      )
      self.dut.bt.bassStopSearching()
      self.assertEqual(source_found_event.source.num_subgroups, 1)
      self.assertEqual(source_found_event.source.sg_number_of_bises[0], 2)
      self.assertEqual(source_found_event.source.broadcast_name, broadcast_name)

  @navi_test_base.named_parameterized(
      dict(
          testcase_name="unencrypted",
          broadcast_code=None,
      ),
      dict(
          testcase_name="encrypted",
          broadcast_code=b"1234567890abcdef",
      ),
  )
  async def test_assistant_add_local_source(
      self, broadcast_code: bytes | None
  ) -> None:
    """Tests adding DUT's broadcast source over BASS from DUT.

    Test steps:
      1. Start broadcasting on DUT.
      2. Add DUT's broadcast source over BASS from DUT.
      3. Wait for add source operation received on REF.
      4. Verify the add source operation on REF.
      5. Wait for sync info on REF.
      6. Wait for BIG info advertisement on REF.
      7. Wait for set broadcast code operation received on REF.
      8. Wait for add source operation complete on REF.

    Args:
      broadcast_code: The broadcast code of the broadcast.
    """
    if not self._bass_enabled:
      self.skipTest("BASS is not enabled.")
    if not self._broadcast_enabled:
      self.skipTest("Broadcast source is not enabled.")

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info(
          "[REF] Set default periodic advertising sync transfer parameters"
      )
      await self.ref.device.send_command(
          hci.HCI_LE_Set_Default_Periodic_Advertising_Sync_Transfer_Parameters_Command(
              mode=0x03, skip=0x00, sync_timeout=0x4000, cte_type=0x00
          ),
          check_result=True,
      )

    await self._prepare_paired_devices()

    self.logger.info("[DUT] Start broadcasting")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      broadcast = await self.dut.bl4a.start_le_audio_broadcast(
          broadcast_code=broadcast_code,
          subgroups=[
              bl4a_api.LeAudioBroadcastSubgroupSettings(
                  quality=android_constants.LeAudioBroadcastQuality.HIGH
              )
          ],
      )

    self.logger.info("[DUT] Broadcast created, ID: %d", broadcast.broadcast_id)

    # Since 25Q1, broadcast will be started only during audio playback.
    self.logger.info("[DUT] Start audio playback")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
    self.dut.bt.audioPlaySine()

    self.logger.info("[DUT] Add broadcast source")
    broadcast_metadata = self.dut.bt.getAllBroadcastMetadata()[0]
    add_source_task = asyncio.create_task(
        asyncio.to_thread(
            lambda: self.dut.bt.bassAddSource(
                self.ref.random_address, broadcast_metadata
            ),
        )
    )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Wait for add source operation")
      operation = await self._ref_bass_service.wait_for_operation(
          bass.AddSourceOperation
      )

    self.assertEqual(operation.broadcast_id, broadcast.broadcast_id)
    self.assertEqual(
        operation.pa_sync,
        bass.PeriodicAdvertisingSyncParams.SYNCHRONIZE_TO_PA_PAST_AVAILABLE,
    )

    pa_syncs = asyncio.Queue[device.PeriodicAdvertisingSync]()

    @self.ref.device.on(device.Device.EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER)
    def _(
        pa_sync: device.PeriodicAdvertisingSync, connection: device.Connection
    ):
      del connection  # Unused.
      pa_syncs.put_nowait(pa_sync)

    receiver_state = bass.BroadcastReceiveState(
        source_id=0,
        source_address=operation.advertiser_address,
        source_adv_sid=operation.advertising_sid,
        broadcast_id=operation.broadcast_id,
        pa_sync_state=bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCINFO_REQUEST,
        big_encryption=bass.BroadcastReceiveState.BigEncryption.NOT_ENCRYPTED,
        bad_code=b"",
        subgroups=operation.subgroups,
    )
    self._ref_bass_service.broadcast_receive_state_characteristic.value = bytes(
        receiver_state
    )
    self.logger.info("[REF] Update broadcast receive state")
    await self.ref.device.notify_subscribers(
        self._ref_bass_service.broadcast_receive_state_characteristic
    )
    self.logger.info("[REF] Wait for sync info")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      while pa_sync := await pa_syncs.get():
        if (
            pa_sync.sid == operation.advertising_sid
            and pa_sync.advertiser_address == operation.advertiser_address
        ):
          self.logger.info("[REF] Sync info received")
          break

    pa_sync = self.ref.device.periodic_advertising_syncs[0]
    biginfo_advertisements = asyncio.Queue[device.BigInfoAdvertisement]()
    pa_sync.on(
        pa_sync.EVENT_BIGINFO_ADVERTISEMENT, biginfo_advertisements.put_nowait
    )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Wait for BIG info advertisement")
      biginfo_advertisement = await biginfo_advertisements.get()

    encryped = (
        device.BigInfoAdvertisement.Encryption.ENCRYPTED
        if broadcast_code
        else device.BigInfoAdvertisement.Encryption.UNENCRYPTED
    )
    self.assertEqual(biginfo_advertisement.encryption, encryped)

    receiver_state.pa_sync_state = (
        bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCHRONIZED_TO_PA
    )
    receiver_state.big_encryption = (
        bass.BroadcastReceiveState.BigEncryption.BROADCAST_CODE_REQUIRED
        if encryped
        else bass.BroadcastReceiveState.BigEncryption.NOT_ENCRYPTED
    )
    self._ref_bass_service.broadcast_receive_state_characteristic.value = bytes(
        receiver_state
    )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Update broadcast receive state")
      await self.ref.device.notify_subscribers(
          self._ref_bass_service.broadcast_receive_state_characteristic
      )

    if encryped:
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
        self.logger.info("[REF] Wait for set broadcast code operation")
        op_set_broadcast_code = await self._ref_bass_service.wait_for_operation(
            bass.SetBroadcastCodeOperation
        )
      self.assertEqual(op_set_broadcast_code.broadcast_code, broadcast_code)

      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
        self.logger.info("[REF] Update broadcast receive state")
        receiver_state.big_encryption = (
            bass.BroadcastReceiveState.BigEncryption.DECRYPTING
        )
        self._ref_bass_service.broadcast_receive_state_characteristic.value = (
            bytes(receiver_state)
        )
        await self.ref.device.notify_subscribers(
            self._ref_bass_service.broadcast_receive_state_characteristic
        )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[DUT] Wait for add source operation complete")
      await add_source_task

  @navi_test_base.named_parameterized(
      dict(
          testcase_name="synced",
          pa_sync_state=bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCHRONIZED_TO_PA,
      ),
      dict(
          testcase_name="unsynced",
          pa_sync_state=bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.NOT_SYNCHRONIZED_TO_PA,
      ),
  )
  async def test_assistant_remove_source(
      self,
      pa_sync_state: bass.BroadcastReceiveState.PeriodicAdvertisingSyncState,
  ) -> None:
    if not self._bass_enabled:
      self.skipTest("BASS is not enabled.")

    # Pretend the broadcast source is already added.
    receiver_state = bass.BroadcastReceiveState(
        source_id=0,
        source_address=hci.Address("00:11:22:33:44:55"),
        source_adv_sid=0,
        broadcast_id=0x123456,
        pa_sync_state=pa_sync_state,
        big_encryption=bass.BroadcastReceiveState.BigEncryption.NOT_ENCRYPTED,
        bad_code=b"",
        subgroups=[],
    )
    broadcast_receive_state_characteristic = (
        self._ref_bass_service.broadcast_receive_state_characteristic
    )
    broadcast_receive_state_characteristic.value = bytes(receiver_state)
    await self._prepare_paired_devices()

    self.logger.info("[DUT] Remove broadcast source")
    remove_source_task = asyncio.create_task(
        asyncio.to_thread(
            lambda: self.dut.bt.bassRemoveSource(self.ref.random_address, 0)
        )
    )

    if (
        pa_sync_state
        == bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCHRONIZED_TO_PA
    ):
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
        self.logger.info("[REF] Wait for modify source operation")
        operation = await self._ref_bass_service.wait_for_operation(
            bass.ModifySourceOperation
        )

      self.assertEqual(operation.source_id, 0)
      self.assertEqual(
          operation.pa_sync,
          bass.PeriodicAdvertisingSyncParams.DO_NOT_SYNCHRONIZE_TO_PA,
      )

      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
        self.logger.info("[REF] Update broadcast receive state")
        receiver_state.pa_sync_state = (
            bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.NOT_SYNCHRONIZED_TO_PA
        )
        broadcast_receive_state_characteristic.value = bytes(receiver_state)
        await self.ref.device.notify_subscribers(
            broadcast_receive_state_characteristic
        )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Wait for remove source operation")
      op_remove_source = await self._ref_bass_service.wait_for_operation(
          bass.RemoveSourceOperation
      )

    self.assertEqual(op_remove_source.source_id, 0)

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Update broadcast receive state")
      broadcast_receive_state_characteristic.value = b""
      await self.ref.device.notify_subscribers(
          broadcast_receive_state_characteristic
      )

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[DUT] Wait for remove source operation complete")
      await remove_source_task

Tests adding DUT's broadcast source over BASS from DUT.

Test steps
  1. Start broadcasting on DUT.
  2. Add DUT's broadcast source over BASS from DUT.
  3. Wait for add source operation received on REF.
  4. Verify the add source operation on REF.
  5. Wait for sync info on REF.
  6. Wait for BIG info advertisement on REF.
  7. Wait for set broadcast code operation received on REF.
  8. Wait for add source operation complete on REF.

Parameters:

Name Type Description Default
broadcast_code bytes | None

The broadcast code of the broadcast.

required
Source code in navi/tests/smoke/broadcast_test.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
@navi_test_base.named_parameterized(
    dict(
        testcase_name="unencrypted",
        broadcast_code=None,
    ),
    dict(
        testcase_name="encrypted",
        broadcast_code=b"1234567890abcdef",
    ),
)
async def test_assistant_add_local_source(
    self, broadcast_code: bytes | None
) -> None:
  """Tests adding DUT's broadcast source over BASS from DUT.

  Test steps:
    1. Start broadcasting on DUT.
    2. Add DUT's broadcast source over BASS from DUT.
    3. Wait for add source operation received on REF.
    4. Verify the add source operation on REF.
    5. Wait for sync info on REF.
    6. Wait for BIG info advertisement on REF.
    7. Wait for set broadcast code operation received on REF.
    8. Wait for add source operation complete on REF.

  Args:
    broadcast_code: The broadcast code of the broadcast.
  """
  if not self._bass_enabled:
    self.skipTest("BASS is not enabled.")
  if not self._broadcast_enabled:
    self.skipTest("Broadcast source is not enabled.")

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info(
        "[REF] Set default periodic advertising sync transfer parameters"
    )
    await self.ref.device.send_command(
        hci.HCI_LE_Set_Default_Periodic_Advertising_Sync_Transfer_Parameters_Command(
            mode=0x03, skip=0x00, sync_timeout=0x4000, cte_type=0x00
        ),
        check_result=True,
    )

  await self._prepare_paired_devices()

  self.logger.info("[DUT] Start broadcasting")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    broadcast = await self.dut.bl4a.start_le_audio_broadcast(
        broadcast_code=broadcast_code,
        subgroups=[
            bl4a_api.LeAudioBroadcastSubgroupSettings(
                quality=android_constants.LeAudioBroadcastQuality.HIGH
            )
        ],
    )

  self.logger.info("[DUT] Broadcast created, ID: %d", broadcast.broadcast_id)

  # Since 25Q1, broadcast will be started only during audio playback.
  self.logger.info("[DUT] Start audio playback")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
  self.dut.bt.audioPlaySine()

  self.logger.info("[DUT] Add broadcast source")
  broadcast_metadata = self.dut.bt.getAllBroadcastMetadata()[0]
  add_source_task = asyncio.create_task(
      asyncio.to_thread(
          lambda: self.dut.bt.bassAddSource(
              self.ref.random_address, broadcast_metadata
          ),
      )
  )

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info("[REF] Wait for add source operation")
    operation = await self._ref_bass_service.wait_for_operation(
        bass.AddSourceOperation
    )

  self.assertEqual(operation.broadcast_id, broadcast.broadcast_id)
  self.assertEqual(
      operation.pa_sync,
      bass.PeriodicAdvertisingSyncParams.SYNCHRONIZE_TO_PA_PAST_AVAILABLE,
  )

  pa_syncs = asyncio.Queue[device.PeriodicAdvertisingSync]()

  @self.ref.device.on(device.Device.EVENT_PERIODIC_ADVERTISING_SYNC_TRANSFER)
  def _(
      pa_sync: device.PeriodicAdvertisingSync, connection: device.Connection
  ):
    del connection  # Unused.
    pa_syncs.put_nowait(pa_sync)

  receiver_state = bass.BroadcastReceiveState(
      source_id=0,
      source_address=operation.advertiser_address,
      source_adv_sid=operation.advertising_sid,
      broadcast_id=operation.broadcast_id,
      pa_sync_state=bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCINFO_REQUEST,
      big_encryption=bass.BroadcastReceiveState.BigEncryption.NOT_ENCRYPTED,
      bad_code=b"",
      subgroups=operation.subgroups,
  )
  self._ref_bass_service.broadcast_receive_state_characteristic.value = bytes(
      receiver_state
  )
  self.logger.info("[REF] Update broadcast receive state")
  await self.ref.device.notify_subscribers(
      self._ref_bass_service.broadcast_receive_state_characteristic
  )
  self.logger.info("[REF] Wait for sync info")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    while pa_sync := await pa_syncs.get():
      if (
          pa_sync.sid == operation.advertising_sid
          and pa_sync.advertiser_address == operation.advertiser_address
      ):
        self.logger.info("[REF] Sync info received")
        break

  pa_sync = self.ref.device.periodic_advertising_syncs[0]
  biginfo_advertisements = asyncio.Queue[device.BigInfoAdvertisement]()
  pa_sync.on(
      pa_sync.EVENT_BIGINFO_ADVERTISEMENT, biginfo_advertisements.put_nowait
  )

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info("[REF] Wait for BIG info advertisement")
    biginfo_advertisement = await biginfo_advertisements.get()

  encryped = (
      device.BigInfoAdvertisement.Encryption.ENCRYPTED
      if broadcast_code
      else device.BigInfoAdvertisement.Encryption.UNENCRYPTED
  )
  self.assertEqual(biginfo_advertisement.encryption, encryped)

  receiver_state.pa_sync_state = (
      bass.BroadcastReceiveState.PeriodicAdvertisingSyncState.SYNCHRONIZED_TO_PA
  )
  receiver_state.big_encryption = (
      bass.BroadcastReceiveState.BigEncryption.BROADCAST_CODE_REQUIRED
      if encryped
      else bass.BroadcastReceiveState.BigEncryption.NOT_ENCRYPTED
  )
  self._ref_bass_service.broadcast_receive_state_characteristic.value = bytes(
      receiver_state
  )

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info("[REF] Update broadcast receive state")
    await self.ref.device.notify_subscribers(
        self._ref_bass_service.broadcast_receive_state_characteristic
    )

  if encryped:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Wait for set broadcast code operation")
      op_set_broadcast_code = await self._ref_bass_service.wait_for_operation(
          bass.SetBroadcastCodeOperation
      )
    self.assertEqual(op_set_broadcast_code.broadcast_code, broadcast_code)

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info("[REF] Update broadcast receive state")
      receiver_state.big_encryption = (
          bass.BroadcastReceiveState.BigEncryption.DECRYPTING
      )
      self._ref_bass_service.broadcast_receive_state_characteristic.value = (
          bytes(receiver_state)
      )
      await self.ref.device.notify_subscribers(
          self._ref_bass_service.broadcast_receive_state_characteristic
      )

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info("[DUT] Wait for add source operation complete")
    await add_source_task

Tests broadcasting on REF, and search from DUT.

Test steps
  1. Start broadcasting on REF.
  2. Search for broadcast on DUT.
  3. Verify the broadcast source found event.

Parameters:

Name Type Description Default
broadcast_code bytes | None

The broadcast code of the broadcast.

required
Source code in navi/tests/smoke/broadcast_test.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
@navi_test_base.named_parameterized(
    dict(
        testcase_name="unencrypted",
        broadcast_code=None,
    ),
    dict(
        testcase_name="encrypted",
        broadcast_code=b"1234567890abcdef",
    ),
)
async def test_broadcast_assist_search(
    self, broadcast_code: bytes | None
) -> None:
  """Tests broadcasting on REF, and search from DUT.

  Test steps:
    1. Start broadcasting on REF.
    2. Search for broadcast on DUT.
    3. Verify the broadcast source found event.

  Args:
    broadcast_code: The broadcast code of the broadcast.
  """
  if not self._bass_enabled:
    self.skipTest("BASS is not enabled.")

  broadcast_id = random.randint(0, 0xFFFFFF)
  broadcast_name = "Bumble Auracast"
  self.logger.info("[REF] Broadcast ID: %d", broadcast_id)

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    await self._start_broadcast_on_ref(
        broadcast_id=broadcast_id,
        broadcast_name=broadcast_name,
        broadcast_code=broadcast_code,
        sampling_frequency=bap.SamplingFrequency.FREQ_48000,
        frame_duration=bap.FrameDuration.DURATION_10000_US,
        octets_per_codec_frame=100,
    )

  with self.dut.bl4a.register_callback(bl4a_api.Module.BASS) as bass_callback:
    self.dut.bt.bassStartSearching()
    self.logger.info("[DUT] Wait for broadcast source found")
    source_found_event = await bass_callback.wait_for_event(
        bl4a_api.BroadcastSourceFound,
        lambda e: (
            cast(bl4a_api.BroadcastSourceFound, e).source.broadcast_id
            == broadcast_id
        ),
    )
    self.dut.bt.bassStopSearching()
    self.assertEqual(source_found_event.source.num_subgroups, 1)
    self.assertEqual(source_found_event.source.sg_number_of_bises[0], 2)
    self.assertEqual(source_found_event.source.broadcast_name, broadcast_name)

Tests broadcasting on DUT, and receiving on REF.

Test steps
  1. Start broadcasting on DUT.
  2. Wait for broadcast advertisement on REF.
  3. Create periodic advertising sync on REF.
  4. Wait for basic audio announcement on REF.
  5. Wait for BIG info advertisement on REF.
  6. Create BIG sync on REF.
  7. Stop broadcasting on DUT.
  8. Wait for BIG sync lost on REF.

Parameters:

Name Type Description Default
quality LeAudioBroadcastQuality

The quality of the broadcast.

required
broadcast_code bytes | None

The broadcast code of the broadcast.

required
expected_sampling_frequency SamplingFrequency

The expected sampling frequency of the broadcast.

required
Source code in navi/tests/smoke/broadcast_test.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
@navi_test_base.named_parameterized(
    standard_quality=dict(
        quality=android_constants.LeAudioBroadcastQuality.STANDARD,
        broadcast_code=None,
        expected_sampling_frequency=bap.SamplingFrequency.FREQ_24000,
    ),
    high_quality=dict(
        quality=android_constants.LeAudioBroadcastQuality.HIGH,
        broadcast_code=None,
        expected_sampling_frequency=bap.SamplingFrequency.FREQ_48000,
    ),
    high_quality_encrypted=dict(
        quality=android_constants.LeAudioBroadcastQuality.HIGH,
        broadcast_code=b"1234567890abcdef",
        expected_sampling_frequency=bap.SamplingFrequency.FREQ_48000,
    ),
)
async def test_broadcast_start_stop(
    self,
    quality: android_constants.LeAudioBroadcastQuality,
    broadcast_code: bytes | None,
    expected_sampling_frequency: bap.SamplingFrequency,
) -> None:
  """Tests broadcasting on DUT, and receiving on REF.

  Test steps:
    1. Start broadcasting on DUT.
    2. Wait for broadcast advertisement on REF.
    3. Create periodic advertising sync on REF.
    4. Wait for basic audio announcement on REF.
    5. Wait for BIG info advertisement on REF.
    6. Create BIG sync on REF.
    7. Stop broadcasting on DUT.
    8. Wait for BIG sync lost on REF.

  Args:
    quality: The quality of the broadcast.
    broadcast_code: The broadcast code of the broadcast.
    expected_sampling_frequency: The expected sampling frequency of the
      broadcast.
  """
  if not self._broadcast_enabled:
    self.skipTest("Broadcast source is not enabled.")

  # TODO: LEHS must be connected before starting broadcast.
  await self._prepare_paired_devices()

  self.logger.info("[DUT] Start broadcasting")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    broadcast = await self.dut.bl4a.start_le_audio_broadcast(
        broadcast_code=broadcast_code,
        subgroups=[
            bl4a_api.LeAudioBroadcastSubgroupSettings(quality=quality)
        ],
    )

  self.logger.info("[DUT] Broadcast created, ID: %d", broadcast.broadcast_id)

  # Since 25Q1, broadcast will be started only during audio playback.
  self.logger.info("[DUT] Start audio playback")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
  self.dut.bt.audioPlaySine()

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    big_sync, basic_audio_announcement = await self._receive_broadcast_on_ref(
        broadcast_id=broadcast.broadcast_id,
        subgroup_index=_SUBGROUP_INDEX,
        broadcast_code=broadcast_code,
    )

  self.assertEqual(
      basic_audio_announcement.subgroups[
          _SUBGROUP_INDEX
      ].codec_specific_configuration.sampling_frequency,
      expected_sampling_frequency,
  )

  sync_lost = asyncio.Event()
  big_sync.once(big_sync.Event.TERMINATION, lambda _: sync_lost.set())

  self.logger.info("[DUT] Stop broadcasting")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    await broadcast.stop()

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info("[REF] Wait for sync lost")
    await sync_lost.wait()

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/classic_host_test.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class ClassicHostTest(navi_test_base.TwoDevicesTestBase):

  @navi_test_base.retry(max_count=2)
  async def test_outgoing_classic_acl(self) -> None:
    """Test outgoing Classic ACL connection.

    Test steps:
      1. Create connection from DUT.
      2. Wait for ACL connected on both devices.
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      self.dut.bt.createBond(
          self.ref.address, android_constants.Transport.CLASSIC
      )

      await self.ref.device.accept(
          f'{self.dut.address}/P',
          timeout=datetime.timedelta(seconds=15).total_seconds(),
      )
      await dut_cb.wait_for_event(
          bl4a_api.AclConnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )
      # disconnect() doesn't work, because it can only remove profile
      # connections.
      self.dut.bt.cancelBond(self.ref.address)
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
          timeout=datetime.timedelta(seconds=30),
      )

  @navi_test_base.retry(max_count=2)
  async def test_incoming_classic_acl(self) -> None:
    """Test incoming Classic ACL connection.

    Test steps:
      1. Create connection from REF.
      2. Wait for ACL connected on both devices.
      3. Disconnect from REF.
      4. Wait for ACL disconnected on both devices.
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      ref_dut_acl = await self.ref.device.connect(
          f'{self.dut.address}/P', transport=core.BT_BR_EDR_TRANSPORT
      )

      await dut_cb.wait_for_event(
          bl4a_api.AclConnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

      await ref_dut_acl.disconnect()
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  @navi_test_base.retry(max_count=2)
  async def test_inquiry(self) -> None:
    """Test inquiry.

    Test steps:
      1. Set REF in discoverable mode.
      2. Start discovery on DUT.
      3. Wait for DUT discovered or timeout(15 seconds).
      4. Check result(should be discovered).
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      await self.ref.device.set_discoverable(True)
      self.dut.bt.startInquiry()

      await dut_cb.wait_for_event(
          bl4a_api.DeviceFound, lambda e: (e.address == self.ref.address)
      )

  async def test_discoverable(self) -> None:
    """Test DUT in discoverable mode.

    Test steps:
      1. Set DUT in discoverable mode.
      2. Start discovery on REF.
      3. Wait for DUT discovered or timeout(15 seconds).
      4. Check result(should be discovered).
    """
    self.dut.bt.setScanMode(android_constants.ScanMode.CONNECTABLE_DISCOVERABLE)

    with pyee_extensions.EventWatcher() as watcher:
      inquiry_future = asyncio.events.get_running_loop().create_future()

      @watcher.on(self.ref.device, 'inquiry_result')
      def on_inquiry_result(address: hci.Address, *_) -> None:
        if address == hci.Address(f'{self.dut.address}/P'):
          inquiry_future.set_result(None)

      await self.ref.device.start_discovery()
      await asyncio.tasks.wait_for(
          inquiry_future, timeout=datetime.timedelta(seconds=15).total_seconds()
      )

  async def test_not_discoverable(self) -> None:
    """Test DUT in not discoverable mode.

    Test steps:
      1. Set DUT in not discoverable mode.
      2. Start discovery on REF.
      3. Wait for DUT discovered or timeout(15 seconds).
      4. Check result(should not be discovered).
    """
    self.dut.bt.setScanMode(android_constants.ScanMode.NONE)

    with pyee_extensions.EventWatcher() as watcher:
      inquiry_future = asyncio.events.get_running_loop().create_future()

      @watcher.on(self.ref.device, 'inquiry_result')
      def on_inquiry_result(address: hci.Address, *_) -> None:
        if address == hci.Address(f'{self.dut.address}/P'):
          inquiry_future.set_result(None)

      await self.ref.device.start_discovery()
      with contextlib.suppress(asyncio.exceptions.TimeoutError):
        await asyncio.tasks.wait_for(
            inquiry_future,
            timeout=datetime.timedelta(seconds=15).total_seconds(),
        )
        asserts.assert_is_none(inquiry_future.result())

Test DUT in discoverable mode.

Test steps
  1. Set DUT in discoverable mode.
  2. Start discovery on REF.
  3. Wait for DUT discovered or timeout(15 seconds).
  4. Check result(should be discovered).
Source code in navi/tests/smoke/classic_host_test.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
async def test_discoverable(self) -> None:
  """Test DUT in discoverable mode.

  Test steps:
    1. Set DUT in discoverable mode.
    2. Start discovery on REF.
    3. Wait for DUT discovered or timeout(15 seconds).
    4. Check result(should be discovered).
  """
  self.dut.bt.setScanMode(android_constants.ScanMode.CONNECTABLE_DISCOVERABLE)

  with pyee_extensions.EventWatcher() as watcher:
    inquiry_future = asyncio.events.get_running_loop().create_future()

    @watcher.on(self.ref.device, 'inquiry_result')
    def on_inquiry_result(address: hci.Address, *_) -> None:
      if address == hci.Address(f'{self.dut.address}/P'):
        inquiry_future.set_result(None)

    await self.ref.device.start_discovery()
    await asyncio.tasks.wait_for(
        inquiry_future, timeout=datetime.timedelta(seconds=15).total_seconds()
    )

Test incoming Classic ACL connection.

Test steps
  1. Create connection from REF.
  2. Wait for ACL connected on both devices.
  3. Disconnect from REF.
  4. Wait for ACL disconnected on both devices.
Source code in navi/tests/smoke/classic_host_test.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@navi_test_base.retry(max_count=2)
async def test_incoming_classic_acl(self) -> None:
  """Test incoming Classic ACL connection.

  Test steps:
    1. Create connection from REF.
    2. Wait for ACL connected on both devices.
    3. Disconnect from REF.
    4. Wait for ACL disconnected on both devices.
  """
  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    ref_dut_acl = await self.ref.device.connect(
        f'{self.dut.address}/P', transport=core.BT_BR_EDR_TRANSPORT
    )

    await dut_cb.wait_for_event(
        bl4a_api.AclConnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        ),
    )

    await ref_dut_acl.disconnect()
    await dut_cb.wait_for_event(
        bl4a_api.AclDisconnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        ),
    )

Test inquiry.

Test steps
  1. Set REF in discoverable mode.
  2. Start discovery on DUT.
  3. Wait for DUT discovered or timeout(15 seconds).
  4. Check result(should be discovered).
Source code in navi/tests/smoke/classic_host_test.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
@navi_test_base.retry(max_count=2)
async def test_inquiry(self) -> None:
  """Test inquiry.

  Test steps:
    1. Set REF in discoverable mode.
    2. Start discovery on DUT.
    3. Wait for DUT discovered or timeout(15 seconds).
    4. Check result(should be discovered).
  """
  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    await self.ref.device.set_discoverable(True)
    self.dut.bt.startInquiry()

    await dut_cb.wait_for_event(
        bl4a_api.DeviceFound, lambda e: (e.address == self.ref.address)
    )

Test DUT in not discoverable mode.

Test steps
  1. Set DUT in not discoverable mode.
  2. Start discovery on REF.
  3. Wait for DUT discovered or timeout(15 seconds).
  4. Check result(should not be discovered).
Source code in navi/tests/smoke/classic_host_test.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
async def test_not_discoverable(self) -> None:
  """Test DUT in not discoverable mode.

  Test steps:
    1. Set DUT in not discoverable mode.
    2. Start discovery on REF.
    3. Wait for DUT discovered or timeout(15 seconds).
    4. Check result(should not be discovered).
  """
  self.dut.bt.setScanMode(android_constants.ScanMode.NONE)

  with pyee_extensions.EventWatcher() as watcher:
    inquiry_future = asyncio.events.get_running_loop().create_future()

    @watcher.on(self.ref.device, 'inquiry_result')
    def on_inquiry_result(address: hci.Address, *_) -> None:
      if address == hci.Address(f'{self.dut.address}/P'):
        inquiry_future.set_result(None)

    await self.ref.device.start_discovery()
    with contextlib.suppress(asyncio.exceptions.TimeoutError):
      await asyncio.tasks.wait_for(
          inquiry_future,
          timeout=datetime.timedelta(seconds=15).total_seconds(),
      )
      asserts.assert_is_none(inquiry_future.result())

Test outgoing Classic ACL connection.

Test steps
  1. Create connection from DUT.
  2. Wait for ACL connected on both devices.
Source code in navi/tests/smoke/classic_host_test.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@navi_test_base.retry(max_count=2)
async def test_outgoing_classic_acl(self) -> None:
  """Test outgoing Classic ACL connection.

  Test steps:
    1. Create connection from DUT.
    2. Wait for ACL connected on both devices.
  """
  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    self.dut.bt.createBond(
        self.ref.address, android_constants.Transport.CLASSIC
    )

    await self.ref.device.accept(
        f'{self.dut.address}/P',
        timeout=datetime.timedelta(seconds=15).total_seconds(),
    )
    await dut_cb.wait_for_event(
        bl4a_api.AclConnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        ),
    )
    # disconnect() doesn't work, because it can only remove profile
    # connections.
    self.dut.bt.cancelBond(self.ref.address)
    await dut_cb.wait_for_event(
        bl4a_api.AclDisconnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        ),
        timeout=datetime.timedelta(seconds=30),
    )

Bases: TwoDevicesTestBase

Tests related to Bluetooth Classic pairing.

Source code in navi/tests/smoke/classic_pairing_test.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
class ClassicPairingTest(navi_test_base.TwoDevicesTestBase):
  """Tests related to Bluetooth Classic pairing."""

  pairing_delegate: pairing_utils.PairingDelegate

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()

  async def _test_ssp_pairing_async(
      self,
      variant: TestVariant,
      pairing_direction: _Direction,
      ref_io_capability: _IoCapability,
      ref_role: _Role,
  ) -> None:
    """Tests Classic SSP pairing.

    Test steps:
      1. Setup configurations.
      2. Make ACL connections.
      3. Start pairing.
      4. Wait for pairing requests and verify pins.
      5. Make actions corresponding to variants.
      6. Verify final states.

    Args:
      variant: Action to perform in the pairing procedure.
      pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
        versa.
      ref_io_capability: IO Capability on the REF device.
      ref_role: HCI role on the REF device.
    """
    pairing_delegate = self.pairing_delegate

    def pairing_config_factory(
        _: device.Connection,
    ) -> pairing.PairingConfig:
      return pairing.PairingConfig(
          sc=True,
          mitm=True,
          bonding=True,
          identity_address_type=pairing.PairingConfig.AddressType.PUBLIC,
          delegate=pairing_delegate,
      )

    self.ref.device.pairing_config_factory = pairing_config_factory

    self.logger.info("[REF] Allow role switch")
    await self.ref.device.send_command(
        hci.HCI_Write_Default_Link_Policy_Settings_Command(
            default_link_policy_settings=0x01
        ),
        check_result=True,
    )

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    ref_addr = str(self.ref.address)

    ref_dut: device.Connection
    auth_task: asyncio.tasks.Task | None = None
    if pairing_direction == _Direction.OUTGOING:
      self.logger.info("[REF] Prepare to accept connection.")
      ref_accept_task = asyncio.tasks.create_task(
          self.ref.device.accept(
              f"{self.dut.address}/P",
              role=ref_role,
              timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
          )
      )
      self.logger.info("[DUT] Create bond and connect implicitly.")
      self.assertTrue(
          self.dut.bt.createBond(ref_addr, android_constants.Transport.CLASSIC)
      )
      self.logger.info("[REF] Accept connection")
      ref_dut = await ref_accept_task
    else:
      self.logger.info("[REF] Connect to DUT.")
      ref_dut = await self.ref.device.connect(
          f"{self.dut.address}/P",
          transport=core.BT_BR_EDR_TRANSPORT,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      self.logger.info("[REF] Create bond.")
      auth_task = asyncio.tasks.create_task(ref_dut.authenticate())
      self.logger.info("[DUT] Wait for incoming connection.")
      await dut_cb.wait_for_event(
          event=bl4a_api.AclConnected(
              address=ref_addr, transport=android_constants.Transport.CLASSIC
          ),
          timeout=_DEFAULT_STEP_TIMEOUT,
      )

    self.logger.info("[DUT] Wait for pairing request.")
    dut_pairing_event = await dut_cb.wait_for_event(
        event=bl4a_api.PairingRequest,
        predicate=lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )
    ref_accept = variant != TestVariant.REJECTED
    dut_accept = variant != TestVariant.REJECT
    ref_answer: pairing_utils.PairingAnswer

    self.logger.info("[DUT] Check reported pairing method.")
    match ref_io_capability:
      case _IoCapability.NO_OUTPUT_NO_INPUT:
        expected_dut_pairing_variant = _AndroidPairingVariant.CONSENT
        expected_ref_pairing_variant = _BumblePairingVariant.JUST_WORK
        ref_answer = ref_accept
      case _IoCapability.KEYBOARD_INPUT_ONLY:
        expected_dut_pairing_variant = _AndroidPairingVariant.DISPLAY_PASSKEY
        expected_ref_pairing_variant = (
            _BumblePairingVariant.PASSKEY_ENTRY_REQUEST
        )
        ref_answer = dut_pairing_event.pin if ref_accept else None
      case _IoCapability.DISPLAY_OUTPUT_ONLY:
        expected_dut_pairing_variant = (
            _AndroidPairingVariant.PASSKEY_CONFIRMATION
        )
        expected_ref_pairing_variant = (
            _BumblePairingVariant.PASSKEY_ENTRY_NOTIFICATION
        )
        # For SSP PASSKEY pairing, Bumble will invoke display_number, and then
        # confirm, so we need to unblock both events.
        pairing_delegate.pairing_answers.put_nowait(None)
        ref_answer = ref_accept
      case _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT:
        expected_dut_pairing_variant = (
            _AndroidPairingVariant.PASSKEY_CONFIRMATION
        )
        expected_ref_pairing_variant = _BumblePairingVariant.NUMERIC_COMPARISON
        ref_answer = ref_accept
      case _:
        raise ValueError(f"Unsupported IO capability: {ref_io_capability}")
    self.assertEqual(dut_pairing_event.variant, expected_dut_pairing_variant)

    self.logger.info("[REF] Wait for pairing request.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_pairing_event = await pairing_delegate.pairing_events.get()

    self.logger.info("[REF] Check reported pairing method.")
    self.assertEqual(ref_pairing_event.variant, expected_ref_pairing_variant)

    if expected_ref_pairing_variant == _BumblePairingVariant.NUMERIC_COMPARISON:
      self.assertEqual(ref_pairing_event.arg, dut_pairing_event.pin)

    self.logger.info("[DUT] Handle pairing confirmation.")
    if dut_accept:
      self.dut.bt.setPairingConfirmation(ref_addr, True)
    else:
      self.dut.bt.cancelBond(ref_addr)

    self.logger.info("[REF] Handle pairing confirmation.")
    if variant == TestVariant.DISCONNECTED:
      await ref_dut.disconnect()
    pairing_delegate.pairing_answers.put_nowait(ref_answer)

    self.logger.info("[DUT] Check final state.")
    expect_state = (
        android_constants.BondState.BONDED
        if variant == TestVariant.ACCEPT
        else android_constants.BondState.NONE
    )
    actual_state = (
        await dut_cb.wait_for_event(
            event=bl4a_api.BondStateChanged,
            predicate=lambda e: (e.state in _TERMINATED_BOND_STATES),
            timeout=_DEFAULT_STEP_TIMEOUT,
        )
    ).state
    self.assertEqual(actual_state, expect_state)

    if auth_task:
      self.logger.info("[REF] Wait authentication complete.")
      expected_errors = (
          []
          if variant == TestVariant.ACCEPT
          else [hci.HCI_Error, asyncio.CancelledError]
      )
      with contextlib.suppress(*expected_errors):
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await auth_task

  async def _test_smp_over_classic_async(
      self,
      expected_key_distribution: _KeyDistribution,
  ) -> None:
    """Tests CTKD procedure with SMP over Classic channel.

    Prerequisite:
      * A Classic ACL connection has been established.

    Test steps:
      1. Initiate or accept CTKD on REF.
      2. Wait for CTKD complete.
      3. Make an LE connection.
         (If IRK is present, RPA wil be used in this stage, otherwise using
         identity address.)
      4. (If LTK present) Encrypt the link to verify LTK.
      5. (If CSRK present) Verify CSRK lierally.

    Args:
      expected_key_distribution: Keys expected to be distributed.
    """
    pairing_delegate = self.pairing_delegate
    assert pairing_delegate is not None

    ref_dut = self.ref.device.find_connection_by_bd_addr(
        hci.Address(f"{self.dut.address}/P")
    )
    assert ref_dut is not None

    # #################################
    # CTKD procedure.
    # #################################
    ref_pairing_future = asyncio.futures.Future[bumble_keys.PairingKeys]()
    ref_key_updates: asyncio.Queue[None] | None = None

    with pyee_extensions.EventWatcher() as watcher:
      # [REF] Watch pairing complete.
      @watcher.once(ref_dut, "pairing")
      def _(keys: bumble_keys.PairingKeys) -> None:
        ref_pairing_future.set_result(keys)

      if _KeyDistribution.DISTRIBUTE_IDENTITY_KEY in expected_key_distribution:
        # [REF] IRK exchange will trigger an async resolving list update.
        ref_key_updates = watcher.async_monitor(
            self.ref.device, "key_store_update"
        )

      self.logger.info("[REF] REF has role=%s.", _Role(ref_dut.role).name)
      pair_task: asyncio.tasks.Task | None = None
      if ref_dut.role == _Role.CENTRAL:
        # [REF] Send SMP pairing request.
        pair_task = asyncio.tasks.create_task(ref_dut.pair())
      else:
        # [REF] Accept SMP pairing request.
        pairing_delegate.acceptions.put_nowait(True)

      self.logger.info("[REF] Wait for CTKD complete.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        if pair_task:
          await pair_task
        if ref_key_updates:
          await ref_key_updates.get()
        keys = await ref_pairing_future

    # If IRK is not exchanged, devices cannot recognize each other from RPA,
    # so they will use identity address for verification.
    ref_address_type = hci.OwnAddressType.PUBLIC
    if _KeyDistribution.DISTRIBUTE_IDENTITY_KEY in expected_key_distribution:
      self.assertIsNotNone(keys.irk)
      ref_address_type = hci.OwnAddressType.RESOLVABLE_OR_PUBLIC

    # #################################
    # IRK & LTK verification.
    # #################################

    self.logger.info("[REF] Create LE L2CAP server and start advertising.")
    ref_l2cap_server = self.ref.device.create_l2cap_server(
        l2cap.LeCreditBasedChannelSpec()
    )
    await self.ref.device.start_advertising(own_address_type=ref_address_type)

    self.logger.info("[DUT] Make LE connection.")
    secure_connection = (
        _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY in expected_key_distribution
    )
    await self.dut.bl4a.create_l2cap_channel(
        address=self.ref.address,
        secure=secure_connection,
        psm=ref_l2cap_server.psm,
        address_type=android_constants.AddressTypeStatus.PUBLIC,
    )

    # #################################
    # CSRK verification.
    # #################################
    if _KeyDistribution.DISTRIBUTE_SIGNING_KEY in expected_key_distribution:
      self.assertIsNotNone(keys.csrk)

  @navi_test_base.parameterized(*(
      (variant, ref_io_capability, ref_role)
      for (variant, ref_io_capability, ref_role) in itertools.product(
          list(TestVariant),
          (
              _IoCapability.NO_OUTPUT_NO_INPUT,
              _IoCapability.KEYBOARD_INPUT_ONLY,
              _IoCapability.DISPLAY_OUTPUT_ONLY,
              _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
          ),
          (_Role.CENTRAL, _Role.PERIPHERAL),
      )
      if not (
          variant == TestVariant.REJECT
          and ref_io_capability == _IoCapability.KEYBOARD_INPUT_ONLY
      )
  ))
  @navi_test_base.retry(max_count=2)
  async def test_outgoing_pairing_ssp_only(
      self,
      variant: TestVariant,
      ref_io_capability: _IoCapability,
      ref_role: _Role,
  ) -> None:
    """Tests outgoing Simple Secure Pairing.

    Test steps:
      1. Perform SSP.

    Args:
      variant: variant of pairing actions performmed in the test.
      ref_io_capability: IO capabilities of the REF device.
      ref_role: ACL role of the REF device.
    """
    # [REF] Disable SMP over Classic L2CAP channel.
    self.ref.device.l2cap_channel_manager.deregister_fixed_channel(
        smp.SMP_BR_CID
    )
    self.pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=ref_io_capability,
        auto_accept=True,
    )
    await self._test_ssp_pairing_async(
        variant=variant,
        pairing_direction=_Direction.OUTGOING,
        ref_io_capability=ref_io_capability,
        ref_role=ref_role,
    )

  @navi_test_base.parameterized(*(
      (variant, ref_io_capability)
      for (variant, ref_io_capability) in itertools.product(
          list(TestVariant),
          (
              _IoCapability.NO_OUTPUT_NO_INPUT,
              _IoCapability.KEYBOARD_INPUT_ONLY,
              _IoCapability.DISPLAY_OUTPUT_ONLY,
              _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
          ),
      )
      if not (
          variant == TestVariant.REJECT
          and ref_io_capability == _IoCapability.KEYBOARD_INPUT_ONLY
      )
  ))
  @navi_test_base.retry(max_count=2)
  async def test_incoming_pairing_ssp_only(
      self,
      variant: TestVariant,
      ref_io_capability: _IoCapability,
  ) -> None:
    """Tests incoming Simple Secure Pairing.

    Test steps:
      1. Perform SSP.

    Args:
      variant: variant of pairing actions performmed in the test.
      ref_io_capability: IO capabilities of the REF device.
    """
    # [REF] Disable SMP over Classic L2CAP channel.
    self.ref.device.l2cap_channel_manager.deregister_fixed_channel(
        smp.SMP_BR_CID
    )
    self.pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=ref_io_capability,
        auto_accept=True,
    )
    await self._test_ssp_pairing_async(
        variant=variant,
        pairing_direction=_Direction.INCOMING,
        ref_io_capability=ref_io_capability,
        ref_role=_Role.CENTRAL,  # unused
    )

  @navi_test_base.parameterized(
      *itertools.product(
          (_Role.CENTRAL, _Role.PERIPHERAL),
          (
              # LTK + IRK
              (
                  _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
                  | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
              ),
              # LTK + IRK + CSRK
              (
                  _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
                  | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
                  | _KeyDistribution.DISTRIBUTE_SIGNING_KEY
              ),
          ),
      ),
  )
  @navi_test_base.retry(max_count=2)
  async def test_outgoing_pairing_ssp_ctkd(
      self,
      ref_role: _Role,
      key_distribution: _KeyDistribution,
  ) -> None:
    """Tests outgoing Simple Secure Pairing with CTKD.

    Test steps:
      1. Perform SSP.
      2. Perform CTKD (Cross-Transport Key Derivation).

    Args:
      ref_role: ACL role of the REF device.
      key_distribution: key distribution in SMP preferred by the REF device.
    """
    ref_io_capability = _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT
    # [REF] Enable SMP over Classic L2CAP channel.
    self.ref.device.l2cap_channel_manager.register_fixed_channel(
        smp.SMP_BR_CID, self.ref.device.on_smp_pdu
    )
    self.pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=ref_io_capability,
        local_initiator_key_distribution=key_distribution,
        local_responder_key_distribution=key_distribution,
        auto_accept=False,
    )
    await self._test_ssp_pairing_async(
        variant=TestVariant.ACCEPT,
        pairing_direction=_Direction.OUTGOING,
        ref_io_capability=ref_io_capability,
        ref_role=ref_role,
    )
    await self._test_smp_over_classic_async(
        expected_key_distribution=key_distribution
    )

  @navi_test_base.retry(max_count=2)
  async def test_legacy_pairing_incoming(self) -> None:
    """Tests incoming Legacy Pairing.

    Test steps:
      1. Disable SSP on REF.
      2. Pair DUT from REF.
      3. Wait for pairing requests on REF.
      4. Set pairing PIN on REF.
      5. Wait for pairing requests on DUT.
      6. Set pairing PIN on DUT.
      7. Verify final states.
    """

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    ref_addr = str(self.ref.address)
    pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=_IoCapability.KEYBOARD_INPUT_ONLY,
        auto_accept=False,
    )

    def pairing_config_factory(
        connection: device.Connection,
    ) -> pairing.PairingConfig:
      del connection
      return pairing.PairingConfig(delegate=pairing_delegate)

    self.ref.device.pairing_config_factory = pairing_config_factory

    self.logger.info("[REF] Disable SSP on REF.")
    await self.ref.device.send_command(
        hci.HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode=0)
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut = await self.ref.device.connect(
        f"{self.dut.address}/P",
        transport=core.BT_BR_EDR_TRANSPORT,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.logger.info("[REF] Create bond.")
    auth_task = asyncio.tasks.create_task(ref_dut.authenticate())
    self.logger.info("[DUT] Wait for incoming connection.")
    await dut_cb.wait_for_event(
        event=bl4a_api.AclConnected(
            address=ref_addr, transport=android_constants.Transport.CLASSIC
        ),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )

    self.logger.info("[REF] Wait for pairing request.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_pairing_request = await pairing_delegate.pairing_events.get()
      self.assertEqual(
          ref_pairing_request.variant,
          _BumblePairingVariant.PIN_CODE_REQUEST,
      )

    self.logger.info("[REF] Handle pairing confirmation.")
    pairing_delegate.pairing_answers.put_nowait(_PIN_CODE_DEFAULT)

    self.logger.info("[DUT] Wait for pairing request.")
    dut_pairing_request = await dut_cb.wait_for_event(
        event=bl4a_api.PairingRequest,
        predicate=lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )
    self.assertEqual(dut_pairing_request.variant, _AndroidPairingVariant.PIN)

    self.logger.info("[DUT] Handle pairing confirmation.")
    self.dut.bt.setPin(ref_addr, _PIN_CODE_DEFAULT)

    self.logger.info("[DUT] Check final state.")
    actual_state = (
        await dut_cb.wait_for_event(
            event=bl4a_api.BondStateChanged,
            predicate=lambda e: (e.state in _TERMINATED_BOND_STATES),
            timeout=_DEFAULT_STEP_TIMEOUT,
        )
    ).state
    self.assertEqual(actual_state, android_constants.BondState.BONDED)

    self.logger.info("[REF] Wait authentication complete.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await auth_task

  @navi_test_base.parameterized(
      *itertools.product((_COD_DEFAULT, _COD_HEADSETS))
  )
  @navi_test_base.retry(max_count=2)
  async def test_legacy_pairing_outgoing(self, ref_cod: int) -> None:
    """Tests outgoing Legacy Pairing.

    Test steps:
      1. Disable SSP on REF.
      2. Pair REF from DUT.
      3. Wait for pairing requests on DUT.
      4. Set pairing PIN on DUT.
      5. Wait for pairing requests on REF.
      6. Set pairing PIN on REF.
      7. Verify final states.

    Args:
      ref_cod: Class of Device code of REF.
    """

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    ref_addr = str(self.ref.address)
    pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=_IoCapability.KEYBOARD_INPUT_ONLY,
        auto_accept=False,
    )

    auto_pair = ref_cod in (_COD_HEADSETS,)
    if auto_pair:
      pin_code = _PIN_CODE_AUTO_PAIR
    else:
      pin_code = _PIN_CODE_DEFAULT

    def pairing_config_factory(
        connection: device.Connection,
    ) -> pairing.PairingConfig:
      del connection
      return pairing.PairingConfig(delegate=pairing_delegate)

    self.ref.device.pairing_config_factory = pairing_config_factory

    self.logger.info("[REF] Set CoD.")
    await self.ref.device.send_command(
        hci.HCI_Write_Class_Of_Device_Command(class_of_device=ref_cod)
    )

    self.logger.info("[REF] Disable SSP on REF.")
    await self.ref.device.send_command(
        hci.HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode=0)
    )

    self.logger.info("[DUT] Search for REF to update CoD.")
    self.dut.bt.startInquiry()
    await dut_cb.wait_for_event(
        event=bl4a_api.DeviceFound,
        predicate=lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )

    self.logger.info("[DUT] Create bond and connect implicitly.")
    self.assertTrue(
        self.dut.bt.createBond(ref_addr, android_constants.Transport.CLASSIC)
    )

    if not auto_pair:
      self.logger.info("[DUT] Wait for pairing request.")
      dut_pairing_request = await dut_cb.wait_for_event(
          event=bl4a_api.PairingRequest,
          predicate=lambda e: (e.address == ref_addr),
          timeout=_DEFAULT_STEP_TIMEOUT,
      )
      self.assertEqual(dut_pairing_request.variant, _AndroidPairingVariant.PIN)

      self.logger.info("[DUT] Handle pairing confirmation.")
      self.dut.bt.setPin(ref_addr, pin_code)

    self.logger.info("[REF] Wait for pairing request.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_pairing_request = await pairing_delegate.pairing_events.get()
      self.assertEqual(
          ref_pairing_request.variant,
          _BumblePairingVariant.PIN_CODE_REQUEST,
      )

    self.logger.info("[REF] Handle pairing confirmation.")
    pairing_delegate.pairing_answers.put_nowait(pin_code)

    self.logger.info("[DUT] Check final state.")
    actual_state = (
        await dut_cb.wait_for_event(
            event=bl4a_api.BondStateChanged,
            predicate=lambda e: (e.state in _TERMINATED_BOND_STATES),
            timeout=_DEFAULT_STEP_TIMEOUT,
        )
    ).state
    self.assertEqual(actual_state, android_constants.BondState.BONDED)

  async def test_remove_bond(self) -> None:
    """Tests removing bond.

    Test steps:
      1. Pair DUT and REF.
      2. Remove bond on DUT.
      3. Verify bond state change on DUT.
    """
    # Prepair pairing.
    await self.classic_connect_and_pair()

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      self.logger.info("[DUT] Remove bond.")
      self.dut.bt.removeBond(self.ref.address)

      self.logger.info("[DUT] Wait for bond state change.")
      await dut_cb.wait_for_event(
          bl4a_api.BondStateChanged(
              address=self.ref.address, state=android_constants.BondState.NONE
          ),
          timeout=_DEFAULT_STEP_TIMEOUT,
      )

Tests incoming Simple Secure Pairing.

Test steps
  1. Perform SSP.

Parameters:

Name Type Description Default
variant TestVariant

variant of pairing actions performmed in the test.

required
ref_io_capability _IoCapability

IO capabilities of the REF device.

required
Source code in navi/tests/smoke/classic_pairing_test.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
@navi_test_base.parameterized(*(
    (variant, ref_io_capability)
    for (variant, ref_io_capability) in itertools.product(
        list(TestVariant),
        (
            _IoCapability.NO_OUTPUT_NO_INPUT,
            _IoCapability.KEYBOARD_INPUT_ONLY,
            _IoCapability.DISPLAY_OUTPUT_ONLY,
            _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        ),
    )
    if not (
        variant == TestVariant.REJECT
        and ref_io_capability == _IoCapability.KEYBOARD_INPUT_ONLY
    )
))
@navi_test_base.retry(max_count=2)
async def test_incoming_pairing_ssp_only(
    self,
    variant: TestVariant,
    ref_io_capability: _IoCapability,
) -> None:
  """Tests incoming Simple Secure Pairing.

  Test steps:
    1. Perform SSP.

  Args:
    variant: variant of pairing actions performmed in the test.
    ref_io_capability: IO capabilities of the REF device.
  """
  # [REF] Disable SMP over Classic L2CAP channel.
  self.ref.device.l2cap_channel_manager.deregister_fixed_channel(
      smp.SMP_BR_CID
  )
  self.pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=ref_io_capability,
      auto_accept=True,
  )
  await self._test_ssp_pairing_async(
      variant=variant,
      pairing_direction=_Direction.INCOMING,
      ref_io_capability=ref_io_capability,
      ref_role=_Role.CENTRAL,  # unused
  )

Tests incoming Legacy Pairing.

Test steps
  1. Disable SSP on REF.
  2. Pair DUT from REF.
  3. Wait for pairing requests on REF.
  4. Set pairing PIN on REF.
  5. Wait for pairing requests on DUT.
  6. Set pairing PIN on DUT.
  7. Verify final states.
Source code in navi/tests/smoke/classic_pairing_test.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
@navi_test_base.retry(max_count=2)
async def test_legacy_pairing_incoming(self) -> None:
  """Tests incoming Legacy Pairing.

  Test steps:
    1. Disable SSP on REF.
    2. Pair DUT from REF.
    3. Wait for pairing requests on REF.
    4. Set pairing PIN on REF.
    5. Wait for pairing requests on DUT.
    6. Set pairing PIN on DUT.
    7. Verify final states.
  """

  dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(dut_cb)
  ref_addr = str(self.ref.address)
  pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=_IoCapability.KEYBOARD_INPUT_ONLY,
      auto_accept=False,
  )

  def pairing_config_factory(
      connection: device.Connection,
  ) -> pairing.PairingConfig:
    del connection
    return pairing.PairingConfig(delegate=pairing_delegate)

  self.ref.device.pairing_config_factory = pairing_config_factory

  self.logger.info("[REF] Disable SSP on REF.")
  await self.ref.device.send_command(
      hci.HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode=0)
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut = await self.ref.device.connect(
      f"{self.dut.address}/P",
      transport=core.BT_BR_EDR_TRANSPORT,
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  self.logger.info("[REF] Create bond.")
  auth_task = asyncio.tasks.create_task(ref_dut.authenticate())
  self.logger.info("[DUT] Wait for incoming connection.")
  await dut_cb.wait_for_event(
      event=bl4a_api.AclConnected(
          address=ref_addr, transport=android_constants.Transport.CLASSIC
      ),
      timeout=_DEFAULT_STEP_TIMEOUT,
  )

  self.logger.info("[REF] Wait for pairing request.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_pairing_request = await pairing_delegate.pairing_events.get()
    self.assertEqual(
        ref_pairing_request.variant,
        _BumblePairingVariant.PIN_CODE_REQUEST,
    )

  self.logger.info("[REF] Handle pairing confirmation.")
  pairing_delegate.pairing_answers.put_nowait(_PIN_CODE_DEFAULT)

  self.logger.info("[DUT] Wait for pairing request.")
  dut_pairing_request = await dut_cb.wait_for_event(
      event=bl4a_api.PairingRequest,
      predicate=lambda e: (e.address == ref_addr),
      timeout=_DEFAULT_STEP_TIMEOUT,
  )
  self.assertEqual(dut_pairing_request.variant, _AndroidPairingVariant.PIN)

  self.logger.info("[DUT] Handle pairing confirmation.")
  self.dut.bt.setPin(ref_addr, _PIN_CODE_DEFAULT)

  self.logger.info("[DUT] Check final state.")
  actual_state = (
      await dut_cb.wait_for_event(
          event=bl4a_api.BondStateChanged,
          predicate=lambda e: (e.state in _TERMINATED_BOND_STATES),
          timeout=_DEFAULT_STEP_TIMEOUT,
      )
  ).state
  self.assertEqual(actual_state, android_constants.BondState.BONDED)

  self.logger.info("[REF] Wait authentication complete.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await auth_task

Tests outgoing Legacy Pairing.

Test steps
  1. Disable SSP on REF.
  2. Pair REF from DUT.
  3. Wait for pairing requests on DUT.
  4. Set pairing PIN on DUT.
  5. Wait for pairing requests on REF.
  6. Set pairing PIN on REF.
  7. Verify final states.

Parameters:

Name Type Description Default
ref_cod int

Class of Device code of REF.

required
Source code in navi/tests/smoke/classic_pairing_test.py
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
@navi_test_base.parameterized(
    *itertools.product((_COD_DEFAULT, _COD_HEADSETS))
)
@navi_test_base.retry(max_count=2)
async def test_legacy_pairing_outgoing(self, ref_cod: int) -> None:
  """Tests outgoing Legacy Pairing.

  Test steps:
    1. Disable SSP on REF.
    2. Pair REF from DUT.
    3. Wait for pairing requests on DUT.
    4. Set pairing PIN on DUT.
    5. Wait for pairing requests on REF.
    6. Set pairing PIN on REF.
    7. Verify final states.

  Args:
    ref_cod: Class of Device code of REF.
  """

  dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(dut_cb)
  ref_addr = str(self.ref.address)
  pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=_IoCapability.KEYBOARD_INPUT_ONLY,
      auto_accept=False,
  )

  auto_pair = ref_cod in (_COD_HEADSETS,)
  if auto_pair:
    pin_code = _PIN_CODE_AUTO_PAIR
  else:
    pin_code = _PIN_CODE_DEFAULT

  def pairing_config_factory(
      connection: device.Connection,
  ) -> pairing.PairingConfig:
    del connection
    return pairing.PairingConfig(delegate=pairing_delegate)

  self.ref.device.pairing_config_factory = pairing_config_factory

  self.logger.info("[REF] Set CoD.")
  await self.ref.device.send_command(
      hci.HCI_Write_Class_Of_Device_Command(class_of_device=ref_cod)
  )

  self.logger.info("[REF] Disable SSP on REF.")
  await self.ref.device.send_command(
      hci.HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode=0)
  )

  self.logger.info("[DUT] Search for REF to update CoD.")
  self.dut.bt.startInquiry()
  await dut_cb.wait_for_event(
      event=bl4a_api.DeviceFound,
      predicate=lambda e: (e.address == ref_addr),
      timeout=_DEFAULT_STEP_TIMEOUT,
  )

  self.logger.info("[DUT] Create bond and connect implicitly.")
  self.assertTrue(
      self.dut.bt.createBond(ref_addr, android_constants.Transport.CLASSIC)
  )

  if not auto_pair:
    self.logger.info("[DUT] Wait for pairing request.")
    dut_pairing_request = await dut_cb.wait_for_event(
        event=bl4a_api.PairingRequest,
        predicate=lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )
    self.assertEqual(dut_pairing_request.variant, _AndroidPairingVariant.PIN)

    self.logger.info("[DUT] Handle pairing confirmation.")
    self.dut.bt.setPin(ref_addr, pin_code)

  self.logger.info("[REF] Wait for pairing request.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_pairing_request = await pairing_delegate.pairing_events.get()
    self.assertEqual(
        ref_pairing_request.variant,
        _BumblePairingVariant.PIN_CODE_REQUEST,
    )

  self.logger.info("[REF] Handle pairing confirmation.")
  pairing_delegate.pairing_answers.put_nowait(pin_code)

  self.logger.info("[DUT] Check final state.")
  actual_state = (
      await dut_cb.wait_for_event(
          event=bl4a_api.BondStateChanged,
          predicate=lambda e: (e.state in _TERMINATED_BOND_STATES),
          timeout=_DEFAULT_STEP_TIMEOUT,
      )
  ).state
  self.assertEqual(actual_state, android_constants.BondState.BONDED)

Tests outgoing Simple Secure Pairing with CTKD.

Test steps
  1. Perform SSP.
  2. Perform CTKD (Cross-Transport Key Derivation).

Parameters:

Name Type Description Default
ref_role _Role

ACL role of the REF device.

required
key_distribution _KeyDistribution

key distribution in SMP preferred by the REF device.

required
Source code in navi/tests/smoke/classic_pairing_test.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
@navi_test_base.parameterized(
    *itertools.product(
        (_Role.CENTRAL, _Role.PERIPHERAL),
        (
            # LTK + IRK
            (
                _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
                | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
            ),
            # LTK + IRK + CSRK
            (
                _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
                | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
                | _KeyDistribution.DISTRIBUTE_SIGNING_KEY
            ),
        ),
    ),
)
@navi_test_base.retry(max_count=2)
async def test_outgoing_pairing_ssp_ctkd(
    self,
    ref_role: _Role,
    key_distribution: _KeyDistribution,
) -> None:
  """Tests outgoing Simple Secure Pairing with CTKD.

  Test steps:
    1. Perform SSP.
    2. Perform CTKD (Cross-Transport Key Derivation).

  Args:
    ref_role: ACL role of the REF device.
    key_distribution: key distribution in SMP preferred by the REF device.
  """
  ref_io_capability = _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT
  # [REF] Enable SMP over Classic L2CAP channel.
  self.ref.device.l2cap_channel_manager.register_fixed_channel(
      smp.SMP_BR_CID, self.ref.device.on_smp_pdu
  )
  self.pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=ref_io_capability,
      local_initiator_key_distribution=key_distribution,
      local_responder_key_distribution=key_distribution,
      auto_accept=False,
  )
  await self._test_ssp_pairing_async(
      variant=TestVariant.ACCEPT,
      pairing_direction=_Direction.OUTGOING,
      ref_io_capability=ref_io_capability,
      ref_role=ref_role,
  )
  await self._test_smp_over_classic_async(
      expected_key_distribution=key_distribution
  )

Tests outgoing Simple Secure Pairing.

Test steps
  1. Perform SSP.

Parameters:

Name Type Description Default
variant TestVariant

variant of pairing actions performmed in the test.

required
ref_io_capability _IoCapability

IO capabilities of the REF device.

required
ref_role _Role

ACL role of the REF device.

required
Source code in navi/tests/smoke/classic_pairing_test.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
@navi_test_base.parameterized(*(
    (variant, ref_io_capability, ref_role)
    for (variant, ref_io_capability, ref_role) in itertools.product(
        list(TestVariant),
        (
            _IoCapability.NO_OUTPUT_NO_INPUT,
            _IoCapability.KEYBOARD_INPUT_ONLY,
            _IoCapability.DISPLAY_OUTPUT_ONLY,
            _IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        ),
        (_Role.CENTRAL, _Role.PERIPHERAL),
    )
    if not (
        variant == TestVariant.REJECT
        and ref_io_capability == _IoCapability.KEYBOARD_INPUT_ONLY
    )
))
@navi_test_base.retry(max_count=2)
async def test_outgoing_pairing_ssp_only(
    self,
    variant: TestVariant,
    ref_io_capability: _IoCapability,
    ref_role: _Role,
) -> None:
  """Tests outgoing Simple Secure Pairing.

  Test steps:
    1. Perform SSP.

  Args:
    variant: variant of pairing actions performmed in the test.
    ref_io_capability: IO capabilities of the REF device.
    ref_role: ACL role of the REF device.
  """
  # [REF] Disable SMP over Classic L2CAP channel.
  self.ref.device.l2cap_channel_manager.deregister_fixed_channel(
      smp.SMP_BR_CID
  )
  self.pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=ref_io_capability,
      auto_accept=True,
  )
  await self._test_ssp_pairing_async(
      variant=variant,
      pairing_direction=_Direction.OUTGOING,
      ref_io_capability=ref_io_capability,
      ref_role=ref_role,
  )

Tests removing bond.

Test steps
  1. Pair DUT and REF.
  2. Remove bond on DUT.
  3. Verify bond state change on DUT.
Source code in navi/tests/smoke/classic_pairing_test.py
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
async def test_remove_bond(self) -> None:
  """Tests removing bond.

  Test steps:
    1. Pair DUT and REF.
    2. Remove bond on DUT.
    3. Verify bond state change on DUT.
  """
  # Prepair pairing.
  await self.classic_connect_and_pair()

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    self.logger.info("[DUT] Remove bond.")
    self.dut.bt.removeBond(self.ref.address)

    self.logger.info("[DUT] Wait for bond state change.")
    await dut_cb.wait_for_event(
        bl4a_api.BondStateChanged(
            address=self.ref.address, state=android_constants.BondState.NONE
        ),
        timeout=_DEFAULT_STEP_TIMEOUT,
    )

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/distance_measurement_test.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
class DistanceMeasurementTest(navi_test_base.TwoDevicesTestBase):
  dut_supported_methods = list[android_constants.DistanceMeasurementMethodId]()
  active_procedure_counter = dict[int, int]()
  ranging_data_table = dict[int, rap.RangingData]()
  completed_ranging_data = asyncio.Queue[rap.RangingData]()

  def _on_subevent_result(
      self, event: hci.HCI_LE_CS_Subevent_Result_Event
  ) -> None:
    if not (
        connection := self.ref.device.lookup_connection(event.connection_handle)
    ):
      return
    procedure_counter = event.procedure_counter
    if not (ranging_data := self.ranging_data_table.get(procedure_counter)):
      ranging_data = self.ranging_data_table[procedure_counter] = (
          rap.RangingData(
              ranging_header=rap.RangingHeader(
                  event.config_id,
                  selected_tx_power=connection.cs_procedures[
                      event.config_id
                  ].selected_tx_power,
                  antenna_paths_mask=(1 << (event.num_antenna_paths + 1)) - 1,
                  ranging_counter=procedure_counter,
              )
          )
      )

    subevent = rap.Subevent(
        start_acl_connection_event=event.start_acl_conn_event_counter,
        frequency_compensation=event.frequency_compensation,
        ranging_abort_reason=event.procedure_done_status,
        ranging_done_status=event.procedure_done_status,
        subevent_done_status=event.subevent_done_status,
        subevent_abort_reason=event.abort_reason,
        reference_power_level=event.reference_power_level,
    )
    ranging_data.subevents.append(subevent)
    self.active_procedure_counter[event.config_id] = procedure_counter
    self._post_subevent_result(event)

  def _post_subevent_result(
      self,
      event: (
          hci.HCI_LE_CS_Subevent_Result_Event
          | hci.HCI_LE_CS_Subevent_Result_Continue_Event
      ),
  ) -> None:
    procedure_counter = self.active_procedure_counter[event.config_id]
    ranging_data = self.ranging_data_table[procedure_counter]
    subevent = ranging_data.subevents[-1]
    subevent.ranging_done_status = event.procedure_done_status
    subevent.subevent_done_status = event.subevent_done_status
    subevent.steps.extend([
        rap.Step(mode, data)
        for mode, data in zip(event.step_mode, event.step_data)
    ])

    if event.procedure_done_status == hci.CsDoneStatus.ALL_RESULTS_COMPLETED:
      self.completed_ranging_data.put_nowait(ranging_data)

  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    if int(self.dut.getprop('ro.build.version.sdk')) <= 35:
      # Unable to receive the supported methods from the API, use the hardcoded
      # list instead.
      self.dut_supported_methods = [
          android_constants.DistanceMeasurementMethodId.AUTO,
          android_constants.DistanceMeasurementMethodId.RSSI,
      ]
    else:
      self.dut_supported_methods = [
          android_constants.DistanceMeasurementMethodId(method_id)
          for method_id in self.dut.bt.getSupportedDistanceMeasurementMethods()
      ]
    self.logger.info('DUT supported methods: %s', self.dut_supported_methods)
    if not self.dut_supported_methods:
      raise signals.TestAbortClass(
          'DUT does not support any distance measurement method.'
      )

    # If DUT supports Channel Sounding, check if the REF device supports it.
    if (
        android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING
        in self.dut_supported_methods
    ):
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
        self.ref.config.channel_sounding_enabled = (
            self.ref.device.host.supports_le_features(
                hci.LeFeatureMask.CHANNEL_SOUNDING
            )
        )

  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self.active_procedure_counter = dict[int, int]()
    self.ranging_data_table = dict[int, rap.RangingData]()
    self.completed_ranging_data = asyncio.Queue[rap.RangingData]()

  async def test_rssi_ranging(self) -> None:
    """Test RSSI ranging."""
    if (
        android_constants.DistanceMeasurementMethodId.RSSI
        not in self.dut_supported_methods
    ):
      self.skipTest('RSSI ranging is not supported, skip the test.')

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      self.logger.info('[REF] Start advertising')
      await self.ref.device.start_advertising(
          own_address_type=hci.OwnAddressType.RANDOM,
          advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
          advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
      )

    # Devices must be connected before starting distance measurement.
    self.logger.info('[DUT] Connect to REF')
    await self.dut.bl4a.connect_gatt_client(
        address=self.ref.random_address,
        transport=android_constants.Transport.LE,
        address_type=android_constants.AddressTypeStatus.RANDOM,
    )
    self.logger.info('[DUT] Start distance measurement')
    distance_measurement = self.dut.bl4a.start_distance_measurement(
        bl4a_api.DistanceMeasurementParameters(
            device=self.ref.random_address,
            method_id=android_constants.DistanceMeasurementMethodId.RSSI,
        ),
    )
    self.logger.info('[DUT] Wait for distance measurement result')
    result = await distance_measurement.wait_for_event(
        bl4a_api.DistanceMeasurementResult
    )
    self.logger.info('Distance: %.2fm', result.result_meters)

  async def test_cs_ranging_outgoing(self) -> None:
    """Test outgoing Channel Sounding ranging.

    Test steps:
      1. Setup Ranging Service on the REF device.
      2. Setup connection and pairing.
      3. Start advertising on the REF device.
      4. Connect to the REF device.
      5. Set default CS settings on the REF device.
      6. Start distance measurement on the DUT device.
      7. Wait for real-time ranging data subscription on the REF device.
      8. Wait for ranging data on the REF device.
      9. Send ranging data from the REF device.
      10. Wait for distance measurement ready on the DUT device.
      11. Wait for distance measurement result on the DUT device.
    """
    if (
        android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING
        not in self.dut_supported_methods
    ):
      self.skipTest('Channel Sounding is not supported, skip the test.')
    if not self.ref.config.channel_sounding_enabled:
      self.skipTest('Channel Sounding is not enabled on the REF device.')

    ras = _RangingService(ras_features=rap.RasFeatures.REAL_TIME_RANGING_DATA)
    self.ref.device.gatt_server.add_service(ras)

    # Setup connection and pairing.
    await self.le_connect_and_pair(ref_address_type=hci.OwnAddressType.RANDOM)

    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM,
        advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
        advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
    )

    # Devices must be connected before starting distance measurement.
    self.logger.info('[DUT] Connect to REF')
    await self.dut.bl4a.connect_gatt_client(
        address=self.ref.random_address,
        transport=android_constants.Transport.LE,
        address_type=android_constants.AddressTypeStatus.RANDOM,
    )

    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address),
        transport=core.BT_LE_TRANSPORT,
    )
    if not ref_dut_acl:
      self.fail('Failed to find ACL connection between DUT and REF.')
    self.logger.info('[REF] Set default CS settings')
    await self.ref.device.set_default_cs_settings(connection=ref_dut_acl)

    subscriptions = asyncio.Queue[None]()
    ras.real_time_ranging_data_characteristic.on(
        'subscription', lambda *_: subscriptions.put_nowait(None)
    )
    self.ref.device.host.on('cs_subevent_result', self._on_subevent_result)
    self.ref.device.host.on(
        'cs_subevent_result_continue', self._post_subevent_result
    )

    self.logger.info('[DUT] Start distance measurement')
    distance_measurement_task = asyncio.create_task(
        asyncio.to_thread(
            lambda: self.dut.bl4a.start_distance_measurement(
                bl4a_api.DistanceMeasurementParameters(
                    device=self.ref.random_address,
                    method_id=android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING,
                ),
            )
        )
    )
    async with self.assert_not_timeout(
        _DEFAULT_TIMEOUT_SECEONDS,
        msg='[DUT] Wait for real-time ranging data subscription',
    ):
      await subscriptions.get()

    async with self.assert_not_timeout(
        _DEFAULT_TIMEOUT_SECEONDS,
        msg='[REF] Wait for ranging data',
    ):
      ranging_data = await self.completed_ranging_data.get()

    async with self.assert_not_timeout(
        _DEFAULT_TIMEOUT_SECEONDS,
        msg='[REF] Send ranging data',
    ):
      await ras.send_real_time_ranging_data(
          connection=next(iter(self.ref.device.connections.values())),
          data=bytes(ranging_data),
      )

    self.logger.info('[DUT] Wait for distance measurement ready')
    distance_measurement = await distance_measurement_task

    self.logger.info('[DUT] Wait for distance measurement result')
    result = await distance_measurement.wait_for_event(
        bl4a_api.DistanceMeasurementResult
    )
    self.logger.info('Distance: %.2fm', result.result_meters)

  async def test_cs_ranging_incoming(self) -> None:
    """Test incoming Channel Sounding ranging.

    Test steps:
      1. Setup connection.
      2. Setup pairing.
      3. Subscribe to real-time ranging data from REF.
      4. Start CS procedure on the REF device.
      5. Wait for ranging data on the REF device.
    """
    if (
        android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING
        not in self.dut_supported_methods
        or not (ref_cs_capabilities := self.ref.device.cs_capabilities)
    ):
      self.skipTest('Channel Sounding is not supported, skip the test.')
    if not self.ref.config.channel_sounding_enabled:
      self.skipTest('Channel Sounding is not enabled on the REF device.')

    # Pairing from REF.
    await self.le_connect_and_pair(
        ref_address_type=hci.OwnAddressType.RANDOM,
        direction=constants.Direction.INCOMING,
    )
    if not (
        ref_dut_acl := self.ref.device.find_connection_by_bd_addr(
            hci.Address(self.dut.address),
            transport=core.BT_LE_TRANSPORT,
        )
    ):
      self.fail('Failed to find ACL connection between DUT and REF.')

    async with device.Peer(ref_dut_acl) as peer:
      ras = peer.create_service_proxy(_RangingServiceClient)
      if not ras:
        self.fail('Failed to create Ranging Service Client.')

    real_time_ranging_data = asyncio.Queue[bytes]()
    await ras.real_time_ranging_data_characteristic.subscribe(
        real_time_ranging_data.put_nowait
    )

    self.logger.info('[REF] Setup Channel Sounding')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      dut_cs_capabilities = await self.ref.device.get_remote_cs_capabilities(
          ref_dut_acl
      )
      await self.ref.device.set_default_cs_settings(ref_dut_acl)
      config = await self.ref.device.create_cs_config(ref_dut_acl)
      await self.ref.device.enable_cs_security(ref_dut_acl)
      tone_antenna_config_selection = _CS_TONE_ANTENNA_CONFIG_MAPPING_TABLE[
          ref_cs_capabilities.num_antennas_supported - 1
      ][dut_cs_capabilities.num_antennas_supported - 1]
      await self.ref.device.set_cs_procedure_parameters(
          connection=ref_dut_acl,
          config=config,
          tone_antenna_config_selection=tone_antenna_config_selection,
          preferred_peer_antenna=_CS_PREFERRED_PEER_ANTENNA_MAPPING_TABLE[
              tone_antenna_config_selection
          ],
      )

    self.logger.info('[REF] Enable CS Procedure')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
      await self.ref.device.enable_cs_procedure(
          connection=ref_dut_acl, config=config
      )

    async with self.assert_not_timeout(
        _DEFAULT_TIMEOUT_SECEONDS,
        msg='[REF] Wait for ranging data',
    ):
      await real_time_ranging_data.get()

Test incoming Channel Sounding ranging.

Test steps
  1. Setup connection.
  2. Setup pairing.
  3. Subscribe to real-time ranging data from REF.
  4. Start CS procedure on the REF device.
  5. Wait for ranging data on the REF device.
Source code in navi/tests/smoke/distance_measurement_test.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
async def test_cs_ranging_incoming(self) -> None:
  """Test incoming Channel Sounding ranging.

  Test steps:
    1. Setup connection.
    2. Setup pairing.
    3. Subscribe to real-time ranging data from REF.
    4. Start CS procedure on the REF device.
    5. Wait for ranging data on the REF device.
  """
  if (
      android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING
      not in self.dut_supported_methods
      or not (ref_cs_capabilities := self.ref.device.cs_capabilities)
  ):
    self.skipTest('Channel Sounding is not supported, skip the test.')
  if not self.ref.config.channel_sounding_enabled:
    self.skipTest('Channel Sounding is not enabled on the REF device.')

  # Pairing from REF.
  await self.le_connect_and_pair(
      ref_address_type=hci.OwnAddressType.RANDOM,
      direction=constants.Direction.INCOMING,
  )
  if not (
      ref_dut_acl := self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          transport=core.BT_LE_TRANSPORT,
      )
  ):
    self.fail('Failed to find ACL connection between DUT and REF.')

  async with device.Peer(ref_dut_acl) as peer:
    ras = peer.create_service_proxy(_RangingServiceClient)
    if not ras:
      self.fail('Failed to create Ranging Service Client.')

  real_time_ranging_data = asyncio.Queue[bytes]()
  await ras.real_time_ranging_data_characteristic.subscribe(
      real_time_ranging_data.put_nowait
  )

  self.logger.info('[REF] Setup Channel Sounding')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    dut_cs_capabilities = await self.ref.device.get_remote_cs_capabilities(
        ref_dut_acl
    )
    await self.ref.device.set_default_cs_settings(ref_dut_acl)
    config = await self.ref.device.create_cs_config(ref_dut_acl)
    await self.ref.device.enable_cs_security(ref_dut_acl)
    tone_antenna_config_selection = _CS_TONE_ANTENNA_CONFIG_MAPPING_TABLE[
        ref_cs_capabilities.num_antennas_supported - 1
    ][dut_cs_capabilities.num_antennas_supported - 1]
    await self.ref.device.set_cs_procedure_parameters(
        connection=ref_dut_acl,
        config=config,
        tone_antenna_config_selection=tone_antenna_config_selection,
        preferred_peer_antenna=_CS_PREFERRED_PEER_ANTENNA_MAPPING_TABLE[
            tone_antenna_config_selection
        ],
    )

  self.logger.info('[REF] Enable CS Procedure')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    await self.ref.device.enable_cs_procedure(
        connection=ref_dut_acl, config=config
    )

  async with self.assert_not_timeout(
      _DEFAULT_TIMEOUT_SECEONDS,
      msg='[REF] Wait for ranging data',
  ):
    await real_time_ranging_data.get()

Test outgoing Channel Sounding ranging.

Test steps
  1. Setup Ranging Service on the REF device.
  2. Setup connection and pairing.
  3. Start advertising on the REF device.
  4. Connect to the REF device.
  5. Set default CS settings on the REF device.
  6. Start distance measurement on the DUT device.
  7. Wait for real-time ranging data subscription on the REF device.
  8. Wait for ranging data on the REF device.
  9. Send ranging data from the REF device.
  10. Wait for distance measurement ready on the DUT device.
  11. Wait for distance measurement result on the DUT device.
Source code in navi/tests/smoke/distance_measurement_test.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
async def test_cs_ranging_outgoing(self) -> None:
  """Test outgoing Channel Sounding ranging.

  Test steps:
    1. Setup Ranging Service on the REF device.
    2. Setup connection and pairing.
    3. Start advertising on the REF device.
    4. Connect to the REF device.
    5. Set default CS settings on the REF device.
    6. Start distance measurement on the DUT device.
    7. Wait for real-time ranging data subscription on the REF device.
    8. Wait for ranging data on the REF device.
    9. Send ranging data from the REF device.
    10. Wait for distance measurement ready on the DUT device.
    11. Wait for distance measurement result on the DUT device.
  """
  if (
      android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING
      not in self.dut_supported_methods
  ):
    self.skipTest('Channel Sounding is not supported, skip the test.')
  if not self.ref.config.channel_sounding_enabled:
    self.skipTest('Channel Sounding is not enabled on the REF device.')

  ras = _RangingService(ras_features=rap.RasFeatures.REAL_TIME_RANGING_DATA)
  self.ref.device.gatt_server.add_service(ras)

  # Setup connection and pairing.
  await self.le_connect_and_pair(ref_address_type=hci.OwnAddressType.RANDOM)

  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM,
      advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
      advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
  )

  # Devices must be connected before starting distance measurement.
  self.logger.info('[DUT] Connect to REF')
  await self.dut.bl4a.connect_gatt_client(
      address=self.ref.random_address,
      transport=android_constants.Transport.LE,
      address_type=android_constants.AddressTypeStatus.RANDOM,
  )

  ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
      hci.Address(self.dut.address),
      transport=core.BT_LE_TRANSPORT,
  )
  if not ref_dut_acl:
    self.fail('Failed to find ACL connection between DUT and REF.')
  self.logger.info('[REF] Set default CS settings')
  await self.ref.device.set_default_cs_settings(connection=ref_dut_acl)

  subscriptions = asyncio.Queue[None]()
  ras.real_time_ranging_data_characteristic.on(
      'subscription', lambda *_: subscriptions.put_nowait(None)
  )
  self.ref.device.host.on('cs_subevent_result', self._on_subevent_result)
  self.ref.device.host.on(
      'cs_subevent_result_continue', self._post_subevent_result
  )

  self.logger.info('[DUT] Start distance measurement')
  distance_measurement_task = asyncio.create_task(
      asyncio.to_thread(
          lambda: self.dut.bl4a.start_distance_measurement(
              bl4a_api.DistanceMeasurementParameters(
                  device=self.ref.random_address,
                  method_id=android_constants.DistanceMeasurementMethodId.CHANNEL_SOUNDING,
              ),
          )
      )
  )
  async with self.assert_not_timeout(
      _DEFAULT_TIMEOUT_SECEONDS,
      msg='[DUT] Wait for real-time ranging data subscription',
  ):
    await subscriptions.get()

  async with self.assert_not_timeout(
      _DEFAULT_TIMEOUT_SECEONDS,
      msg='[REF] Wait for ranging data',
  ):
    ranging_data = await self.completed_ranging_data.get()

  async with self.assert_not_timeout(
      _DEFAULT_TIMEOUT_SECEONDS,
      msg='[REF] Send ranging data',
  ):
    await ras.send_real_time_ranging_data(
        connection=next(iter(self.ref.device.connections.values())),
        data=bytes(ranging_data),
    )

  self.logger.info('[DUT] Wait for distance measurement ready')
  distance_measurement = await distance_measurement_task

  self.logger.info('[DUT] Wait for distance measurement result')
  result = await distance_measurement.wait_for_event(
      bl4a_api.DistanceMeasurementResult
  )
  self.logger.info('Distance: %.2fm', result.result_meters)

Test RSSI ranging.

Source code in navi/tests/smoke/distance_measurement_test.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
async def test_rssi_ranging(self) -> None:
  """Test RSSI ranging."""
  if (
      android_constants.DistanceMeasurementMethodId.RSSI
      not in self.dut_supported_methods
  ):
    self.skipTest('RSSI ranging is not supported, skip the test.')

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECEONDS):
    self.logger.info('[REF] Start advertising')
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM,
        advertising_interval_min=_DEFAULT_ADVERTISING_INTERVAL,
        advertising_interval_max=_DEFAULT_ADVERTISING_INTERVAL,
    )

  # Devices must be connected before starting distance measurement.
  self.logger.info('[DUT] Connect to REF')
  await self.dut.bl4a.connect_gatt_client(
      address=self.ref.random_address,
      transport=android_constants.Transport.LE,
      address_type=android_constants.AddressTypeStatus.RANDOM,
  )
  self.logger.info('[DUT] Start distance measurement')
  distance_measurement = self.dut.bl4a.start_distance_measurement(
      bl4a_api.DistanceMeasurementParameters(
          device=self.ref.random_address,
          method_id=android_constants.DistanceMeasurementMethodId.RSSI,
      ),
  )
  self.logger.info('[DUT] Wait for distance measurement result')
  result = await distance_measurement.wait_for_event(
      bl4a_api.DistanceMeasurementResult
  )
  self.logger.info('Distance: %.2fm', result.result_meters)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/gatt_client_test.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
class GattClientTest(navi_test_base.TwoDevicesTestBase):

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.getprop(android_constants.Property.GATT_ENABLED) != "true":
      raise signals.TestAbortClass("GATT is not enabled on DUT.")

  async def test_discover_services(self) -> None:
    """Test connect GATT as client."""
    service_uuid = str(uuid.uuid4())
    self.ref.device.add_service(
        gatt.Service(uuid=service_uuid, characteristics=[])
    )

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM
    )
    self.logger.info("[DUT] Connect to REF.")
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        str(self.ref.random_address),
        android_constants.Transport.LE,
        android_constants.AddressTypeStatus.RANDOM,
    )

    self.logger.info("[DUT] Discover services.")
    services = await gatt_client.discover_services()

    self.logger.info("[DUT] Check services.")
    asserts.assert_true(
        any(service.uuid == service_uuid for service in services),
        "Cannot find service UUID?",
    )

  async def test_write_characteristic(self) -> None:
    """Test write value to characteristics.

    Test steps:
      1. Add a GATT server with a writable characteristic on REF.
      2. Start advertising on REF.
      3. Connect GATT(and LE-ACL) to REF from DUT.
      4. Discover GATT services from DUT.
      5. Write characteristic value on REF from DUT.
      6. Check written value.
    """
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())

    write_future = asyncio.get_running_loop().create_future()

    def on_write(connection: device.Connection, value: bytes) -> None:
      del connection  # Unused.
      write_future.set_result(value)

    self.ref.device.add_service(
        gatt.Service(
            uuid=service_uuid,
            characteristics=[
                gatt.Characteristic(
                    uuid=characteristic_uuid,
                    properties=gatt.Characteristic.Properties.WRITE,
                    permissions=gatt.Characteristic.Permissions.WRITEABLE,
                    value=gatt.CharacteristicValue(write=on_write),
                )
            ],
        )
    )

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM
    )
    self.logger.info("[DUT] Connect to REF.")
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        str(self.ref.random_address),
        android_constants.Transport.LE,
        android_constants.AddressTypeStatus.RANDOM,
    )
    self.logger.info("[DUT] Discover services.")
    services = await gatt_client.discover_services()
    characteristic = bl4a_api.find_characteristic_by_uuid(
        characteristic_uuid, services
    )
    if not characteristic.handle:
      self.fail("Cannot find characteristic.")

    self.logger.info("[DUT] Write characteristic.")
    expected_value = secrets.token_bytes(16)
    await gatt_client.write_characteristic(
        characteristic.handle,
        expected_value,
        android_constants.GattWriteType.DEFAULT,
    )
    self.logger.info("[REF] Check write value.")
    asserts.assert_equal(expected_value, await write_future)

  async def test_characteristic_notification(self) -> None:
    """Test read value from characteristics.

    Test steps:
      1. Add a GATT server with a readable characteristic on REF.
      2. Start advertising on REF.
      3. Connect GATT(and LE-ACL) to REF from DUT.
      4. Discover GATT services from DUT.
      5. Read characteristic value on REF from DUT.
      6. Check read value.
    """
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())
    expected_value = secrets.token_bytes(256)

    self.ref.device.add_service(
        gatt.Service(
            uuid=service_uuid,
            characteristics=[
                gatt.Characteristic(
                    uuid=characteristic_uuid,
                    properties=gatt.Characteristic.Properties.READ,
                    permissions=gatt.Characteristic.Permissions.READABLE,
                    value=expected_value,
                )
            ],
        )
    )

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM
    )
    self.logger.info("[DUT] Connect to REF.")
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        str(self.ref.random_address),
        android_constants.Transport.LE,
        android_constants.AddressTypeStatus.RANDOM,
    )
    self.logger.info("[DUT] Discover services.")
    services = await gatt_client.discover_services()
    characteristic = bl4a_api.find_characteristic_by_uuid(
        characteristic_uuid, services
    )
    if not characteristic.handle:
      self.fail("Cannot find characteristic.")

    self.logger.info("[DUT] Read characteristic.")
    actual_value = await gatt_client.read_characteristic(characteristic.handle)
    self.logger.info("Check read value.")
    asserts.assert_equal(expected_value, actual_value)

  async def test_subscribe_characteristic(self) -> None:
    """Test subscribe value from characteristics.

    Test steps:
      1. Add a GATT server with a notifyable characteristic on REF.
      2. Start advertising on REF.
      3. Connect GATT(and LE-ACL) to REF from DUT.
      4. Discover GATT services from DUT.
      5. Subscribe characteristic value on REF from DUT.
      6. Notify subscribers from REF.
      7. Check read value.
    """
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())
    expected_value = secrets.token_bytes(256)

    ref_characteristic = gatt.Characteristic(
        uuid=characteristic_uuid,
        properties=(
            gatt.Characteristic.Properties.READ
            | gatt.Characteristic.Properties.NOTIFY
        ),
        permissions=gatt.Characteristic.Permissions.READABLE,
        value=expected_value,
    )
    self.ref.device.add_service(
        gatt.Service(
            uuid=service_uuid,
            characteristics=[ref_characteristic],
        )
    )

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM
    )
    self.logger.info("[DUT] Connect to REF.")
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        str(self.ref.random_address),
        android_constants.Transport.LE,
        android_constants.AddressTypeStatus.RANDOM,
    )
    self.logger.info("[DUT] Discover services.")
    services = await gatt_client.discover_services()
    characteristic = bl4a_api.find_characteristic_by_uuid(
        characteristic_uuid, services
    )
    if not characteristic.handle:
      self.fail("Cannot find characteristic.")

    self.logger.info("[DUT] Subscribe characteristic.")
    await gatt_client.subscribe_characteristic_notifications(
        characteristic.handle
    )

    self.logger.info("[REF] Notify subscribers.")
    expected_value = secrets.token_bytes(16)
    await self.ref.device.notify_subscribers(ref_characteristic, expected_value)

    self.logger.info("Check notified value.")
    notification = await gatt_client.wait_for_event(
        bl4a_api.GattCharacteristicChanged,
        lambda e: (e.handle == characteristic.handle),
        datetime.timedelta(seconds=10),
    )
    asserts.assert_equal(expected_value, notification.value)

Test read value from characteristics.

Test steps
  1. Add a GATT server with a readable characteristic on REF.
  2. Start advertising on REF.
  3. Connect GATT(and LE-ACL) to REF from DUT.
  4. Discover GATT services from DUT.
  5. Read characteristic value on REF from DUT.
  6. Check read value.
Source code in navi/tests/smoke/gatt_client_test.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
async def test_characteristic_notification(self) -> None:
  """Test read value from characteristics.

  Test steps:
    1. Add a GATT server with a readable characteristic on REF.
    2. Start advertising on REF.
    3. Connect GATT(and LE-ACL) to REF from DUT.
    4. Discover GATT services from DUT.
    5. Read characteristic value on REF from DUT.
    6. Check read value.
  """
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())
  expected_value = secrets.token_bytes(256)

  self.ref.device.add_service(
      gatt.Service(
          uuid=service_uuid,
          characteristics=[
              gatt.Characteristic(
                  uuid=characteristic_uuid,
                  properties=gatt.Characteristic.Properties.READ,
                  permissions=gatt.Characteristic.Permissions.READABLE,
                  value=expected_value,
              )
          ],
      )
  )

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM
  )
  self.logger.info("[DUT] Connect to REF.")
  gatt_client = await self.dut.bl4a.connect_gatt_client(
      str(self.ref.random_address),
      android_constants.Transport.LE,
      android_constants.AddressTypeStatus.RANDOM,
  )
  self.logger.info("[DUT] Discover services.")
  services = await gatt_client.discover_services()
  characteristic = bl4a_api.find_characteristic_by_uuid(
      characteristic_uuid, services
  )
  if not characteristic.handle:
    self.fail("Cannot find characteristic.")

  self.logger.info("[DUT] Read characteristic.")
  actual_value = await gatt_client.read_characteristic(characteristic.handle)
  self.logger.info("Check read value.")
  asserts.assert_equal(expected_value, actual_value)

Test connect GATT as client.

Source code in navi/tests/smoke/gatt_client_test.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
async def test_discover_services(self) -> None:
  """Test connect GATT as client."""
  service_uuid = str(uuid.uuid4())
  self.ref.device.add_service(
      gatt.Service(uuid=service_uuid, characteristics=[])
  )

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM
  )
  self.logger.info("[DUT] Connect to REF.")
  gatt_client = await self.dut.bl4a.connect_gatt_client(
      str(self.ref.random_address),
      android_constants.Transport.LE,
      android_constants.AddressTypeStatus.RANDOM,
  )

  self.logger.info("[DUT] Discover services.")
  services = await gatt_client.discover_services()

  self.logger.info("[DUT] Check services.")
  asserts.assert_true(
      any(service.uuid == service_uuid for service in services),
      "Cannot find service UUID?",
  )

Test subscribe value from characteristics.

Test steps
  1. Add a GATT server with a notifyable characteristic on REF.
  2. Start advertising on REF.
  3. Connect GATT(and LE-ACL) to REF from DUT.
  4. Discover GATT services from DUT.
  5. Subscribe characteristic value on REF from DUT.
  6. Notify subscribers from REF.
  7. Check read value.
Source code in navi/tests/smoke/gatt_client_test.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
async def test_subscribe_characteristic(self) -> None:
  """Test subscribe value from characteristics.

  Test steps:
    1. Add a GATT server with a notifyable characteristic on REF.
    2. Start advertising on REF.
    3. Connect GATT(and LE-ACL) to REF from DUT.
    4. Discover GATT services from DUT.
    5. Subscribe characteristic value on REF from DUT.
    6. Notify subscribers from REF.
    7. Check read value.
  """
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())
  expected_value = secrets.token_bytes(256)

  ref_characteristic = gatt.Characteristic(
      uuid=characteristic_uuid,
      properties=(
          gatt.Characteristic.Properties.READ
          | gatt.Characteristic.Properties.NOTIFY
      ),
      permissions=gatt.Characteristic.Permissions.READABLE,
      value=expected_value,
  )
  self.ref.device.add_service(
      gatt.Service(
          uuid=service_uuid,
          characteristics=[ref_characteristic],
      )
  )

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM
  )
  self.logger.info("[DUT] Connect to REF.")
  gatt_client = await self.dut.bl4a.connect_gatt_client(
      str(self.ref.random_address),
      android_constants.Transport.LE,
      android_constants.AddressTypeStatus.RANDOM,
  )
  self.logger.info("[DUT] Discover services.")
  services = await gatt_client.discover_services()
  characteristic = bl4a_api.find_characteristic_by_uuid(
      characteristic_uuid, services
  )
  if not characteristic.handle:
    self.fail("Cannot find characteristic.")

  self.logger.info("[DUT] Subscribe characteristic.")
  await gatt_client.subscribe_characteristic_notifications(
      characteristic.handle
  )

  self.logger.info("[REF] Notify subscribers.")
  expected_value = secrets.token_bytes(16)
  await self.ref.device.notify_subscribers(ref_characteristic, expected_value)

  self.logger.info("Check notified value.")
  notification = await gatt_client.wait_for_event(
      bl4a_api.GattCharacteristicChanged,
      lambda e: (e.handle == characteristic.handle),
      datetime.timedelta(seconds=10),
  )
  asserts.assert_equal(expected_value, notification.value)

Test write value to characteristics.

Test steps
  1. Add a GATT server with a writable characteristic on REF.
  2. Start advertising on REF.
  3. Connect GATT(and LE-ACL) to REF from DUT.
  4. Discover GATT services from DUT.
  5. Write characteristic value on REF from DUT.
  6. Check written value.
Source code in navi/tests/smoke/gatt_client_test.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
async def test_write_characteristic(self) -> None:
  """Test write value to characteristics.

  Test steps:
    1. Add a GATT server with a writable characteristic on REF.
    2. Start advertising on REF.
    3. Connect GATT(and LE-ACL) to REF from DUT.
    4. Discover GATT services from DUT.
    5. Write characteristic value on REF from DUT.
    6. Check written value.
  """
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())

  write_future = asyncio.get_running_loop().create_future()

  def on_write(connection: device.Connection, value: bytes) -> None:
    del connection  # Unused.
    write_future.set_result(value)

  self.ref.device.add_service(
      gatt.Service(
          uuid=service_uuid,
          characteristics=[
              gatt.Characteristic(
                  uuid=characteristic_uuid,
                  properties=gatt.Characteristic.Properties.WRITE,
                  permissions=gatt.Characteristic.Permissions.WRITEABLE,
                  value=gatt.CharacteristicValue(write=on_write),
              )
          ],
      )
  )

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM
  )
  self.logger.info("[DUT] Connect to REF.")
  gatt_client = await self.dut.bl4a.connect_gatt_client(
      str(self.ref.random_address),
      android_constants.Transport.LE,
      android_constants.AddressTypeStatus.RANDOM,
  )
  self.logger.info("[DUT] Discover services.")
  services = await gatt_client.discover_services()
  characteristic = bl4a_api.find_characteristic_by_uuid(
      characteristic_uuid, services
  )
  if not characteristic.handle:
    self.fail("Cannot find characteristic.")

  self.logger.info("[DUT] Write characteristic.")
  expected_value = secrets.token_bytes(16)
  await gatt_client.write_characteristic(
      characteristic.handle,
      expected_value,
      android_constants.GattWriteType.DEFAULT,
  )
  self.logger.info("[REF] Check write value.")
  asserts.assert_equal(expected_value, await write_future)

Bases: TwoDevicesTestBase

Tests of GATT server implementation on Pixel.

Source code in navi/tests/smoke/gatt_server_test.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class GattServerTest(navi_test_base.TwoDevicesTestBase):
  """Tests of GATT server implementation on Pixel."""

  dut_gatt_server: bl4a_api.GattServer
  dut_advertiser: bl4a_api.LegacyAdvertiser

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.getprop(android_constants.Property.GATT_ENABLED) != "true":
      raise signals.TestAbortClass("GATT is not enabled on DUT.")

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self.logger.info("[DUT] Open server.")
    self.dut_gatt_server = self.dut.bl4a.create_gatt_server()

    self.logger.info("[DUT] Start advertising.")
    self.dut_advertiser = await self.dut.bl4a.start_legacy_advertiser(
        bl4a_api.LegacyAdvertiseSettings(
            own_address_type=android_constants.AddressTypeStatus.PUBLIC,
            advertise_mode=android_constants.LegacyAdvertiseMode.LOW_LATENCY,
        ),
    )

  @override
  async def async_teardown_test(self) -> None:
    await super().async_teardown_test()
    self.dut_gatt_server.close()
    self.dut_advertiser.stop()

  @retry.retry_on_exception()
  async def _make_le_connection(self) -> bumble.device.Connection:
    """Connects to DUT over LE and returns the connection."""
    ref_dut_acl = await self.ref.device.connect(
        f"{self.dut.address}/P",
        transport=bumble.core.BT_LE_TRANSPORT,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
        own_address_type=hci.OwnAddressType.RANDOM,
    )
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_dut_acl.get_remote_le_features()
    return ref_dut_acl

  async def test_add_service(self) -> None:
    """Tests opening a GATT server on DUT, adding a service discovered by REF.

    Test steps:
      1. Open a GATT server on DUT.
      2. Add a GATT service to the server instance.
      3. Discover services from REF.
      4. Verify added service is discovered.
    """
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())

    self.logger.info("[DUT] Add a service.")
    await self.dut_gatt_server.add_service(
        bl4a_api.GattService(
            uuid=service_uuid,
            characteristics=[
                bl4a_api.GattCharacteristic(
                    uuid=characteristic_uuid,
                    properties=_Property.READ,
                    permissions=_Permission.READ,
                )
            ],
        ),
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self._make_le_connection()

    async with bumble.device.Peer(ref_dut_acl) as peer:
      self.logger.info("[REF] Check services.")
      services = peer.get_services_by_uuid(bumble.core.UUID(service_uuid))
      self.assertLen(services, 1)
      characteristics = services[0].get_characteristics_by_uuid(
          bumble.core.UUID(characteristic_uuid)
      )
      self.assertLen(characteristics, 1)
      self.assertEqual(
          characteristics[0].properties, gatt.Characteristic.Properties.READ
      )

  async def test_handle_characteristic_read_request(self) -> None:
    """Tests handling a characteristic read request.

    Test steps:
      1. Open a GATT server on DUT.
      2. Add a GATT service including a readable characteristic to the server
      instance.
      3. Read characteristic from REF.
      4. Handle the read request and send response from DUT.
      5. Check read result from REF.
    """
    # UUID must be random here, otherwise there might be interference when
    # multiple tests run in the same box.
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())

    self.logger.info("[DUT] Add a service.")
    await self.dut_gatt_server.add_service(
        bl4a_api.GattService(
            uuid=service_uuid,
            characteristics=[
                bl4a_api.GattCharacteristic(
                    uuid=characteristic_uuid,
                    properties=_Property.READ,
                    permissions=_Permission.READ,
                )
            ],
        ),
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self._make_le_connection()

    async with bumble.device.Peer(ref_dut_acl) as peer:
      characteristic = peer.get_characteristics_by_uuid(
          bumble.core.UUID(characteristic_uuid)
      )[0]

      self.logger.info("[REF] Read characteristic.")
      read_task = asyncio.create_task(characteristic.read_value())

      read_request = await self.dut_gatt_server.wait_for_event(
          event=bl4a_api.GattCharacteristicReadRequest,
          predicate=lambda request: (
              request.characteristic_uuid == characteristic_uuid
          ),
      )
      expected_data = secrets.token_bytes(16)
      self.dut_gatt_server.send_response(
          address=read_request.address,
          request_id=read_request.request_id,
          status=android_constants.GattStatus.SUCCESS,
          value=expected_data,
      )
      self.assertEqual(await read_task, expected_data)

  async def test_handle_characteristic_write_request(self) -> None:
    """Tests handling a characteristic write request.

    Test steps:
      1. Open a GATT server on DUT.
      2. Add a GATT service including a writable characteristic to the server
      instance.
      3. Write characteristic from REF.
      4. Handle the write request and send response from DUT.
      5. Check write result from REF.
    """
    # UUID must be random here, otherwise there might be interference when
    # multiple tests run in the same box.
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())

    self.logger.info("[DUT] Add a service.")
    await self.dut_gatt_server.add_service(
        bl4a_api.GattService(
            uuid=service_uuid,
            characteristics=[
                bl4a_api.GattCharacteristic(
                    uuid=characteristic_uuid,
                    properties=_Property.WRITE,
                    permissions=_Permission.WRITE,
                )
            ],
        ),
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self._make_le_connection()

    async with bumble.device.Peer(ref_dut_acl) as peer:
      characteristic = peer.get_characteristics_by_uuid(
          bumble.core.UUID(characteristic_uuid)
      )[0]

      self.logger.info("[REF] Write characteristic.")
      expected_data = secrets.token_bytes(16)
      write_task = asyncio.create_task(
          characteristic.write_value(expected_data, with_response=True)
      )

      write_request = await self.dut_gatt_server.wait_for_event(
          event=bl4a_api.GattCharacteristicWriteRequest,
          predicate=lambda request: (
              request.characteristic_uuid == characteristic_uuid
          ),
      )
      self.assertEqual(write_request.value, expected_data)

      self.dut_gatt_server.send_response(
          address=write_request.address,
          request_id=write_request.request_id,
          status=android_constants.GattStatus.SUCCESS,
          value=b"",
      )
      await write_task

  async def test_notify(self) -> None:
    """Tests sending GATT notification.

    Test steps:
      1. Add a GATT service including a characteristic to the server instance.
      2. Subscribe GATT characteristic from REF.
      3. Handle the subscribe request (CCCD write) from DUT.
      4. Send notification from DUT.
      5. Check notification from REF.
    """
    # UUID must be random here, otherwise there might be interference when
    # multiple tests run in the same box.
    service_uuid = str(uuid.uuid4())
    characteristic_uuid = str(uuid.uuid4())

    self.logger.info("[DUT] Add a service.")
    await self.dut_gatt_server.add_service(
        bl4a_api.GattService(
            uuid=service_uuid,
            characteristics=[
                bl4a_api.GattCharacteristic(
                    uuid=characteristic_uuid,
                    properties=_Property.READ | _Property.NOTIFY,
                    permissions=_Permission.READ,
                    descriptors=[
                        bl4a_api.GattDescriptor(
                            uuid=_CCCD_UUID,
                            permissions=_Permission.READ | _Permission.WRITE,
                        )
                    ],
                )
            ],
        ),
    )
    dut_characteristic = bl4a_api.find_characteristic_by_uuid(
        characteristic_uuid, self.dut_gatt_server.services
    )
    if not dut_characteristic.handle:
      self.fail("Cannot find characteristic.")

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self._make_le_connection()

    async with bumble.device.Peer(ref_dut_acl) as peer:
      ref_characteristic = peer.get_characteristics_by_uuid(
          bumble.core.UUID(characteristic_uuid)
      )[0]

      self.logger.info("[REF] Subscribe characteristic.")
      notification_queue = asyncio.Queue[bytes]()
      expected_data = secrets.token_bytes(16)
      subscribe_task = asyncio.create_task(
          ref_characteristic.subscribe(notification_queue.put_nowait)
      )

      self.logger.info("[DUT] Wait for CCCD write.")
      subscribe_request = await self.dut_gatt_server.wait_for_event(
          event=bl4a_api.GattDescriptorWriteRequest,
          predicate=lambda request: (
              request.characteristic_handle == dut_characteristic.handle
              and request.descriptor_uuid == _CCCD_UUID
          ),
      )

      self.logger.info("[DUT] Respond to CCCD write.")
      self.dut_gatt_server.send_response(
          address=subscribe_request.address,
          request_id=subscribe_request.request_id,
          status=android_constants.GattStatus.SUCCESS,
          value=b"",
      )

      self.logger.info("[REF] Wait subscription complete.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await subscribe_task

      self.logger.info("[DUT] Send notification.")
      self.dut_gatt_server.send_notification(
          address=self.ref.random_address,
          characteristic_handle=dut_characteristic.handle,
          confirm=False,
          value=expected_data,
      )

      self.logger.info("[REF] Wait for notification.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(await notification_queue.get(), expected_data)

Tests opening a GATT server on DUT, adding a service discovered by REF.

Test steps
  1. Open a GATT server on DUT.
  2. Add a GATT service to the server instance.
  3. Discover services from REF.
  4. Verify added service is discovered.
Source code in navi/tests/smoke/gatt_server_test.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
async def test_add_service(self) -> None:
  """Tests opening a GATT server on DUT, adding a service discovered by REF.

  Test steps:
    1. Open a GATT server on DUT.
    2. Add a GATT service to the server instance.
    3. Discover services from REF.
    4. Verify added service is discovered.
  """
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())

  self.logger.info("[DUT] Add a service.")
  await self.dut_gatt_server.add_service(
      bl4a_api.GattService(
          uuid=service_uuid,
          characteristics=[
              bl4a_api.GattCharacteristic(
                  uuid=characteristic_uuid,
                  properties=_Property.READ,
                  permissions=_Permission.READ,
              )
          ],
      ),
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self._make_le_connection()

  async with bumble.device.Peer(ref_dut_acl) as peer:
    self.logger.info("[REF] Check services.")
    services = peer.get_services_by_uuid(bumble.core.UUID(service_uuid))
    self.assertLen(services, 1)
    characteristics = services[0].get_characteristics_by_uuid(
        bumble.core.UUID(characteristic_uuid)
    )
    self.assertLen(characteristics, 1)
    self.assertEqual(
        characteristics[0].properties, gatt.Characteristic.Properties.READ
    )

Tests handling a characteristic read request.

Test steps
  1. Open a GATT server on DUT.
  2. Add a GATT service including a readable characteristic to the server instance.
  3. Read characteristic from REF.
  4. Handle the read request and send response from DUT.
  5. Check read result from REF.
Source code in navi/tests/smoke/gatt_server_test.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
async def test_handle_characteristic_read_request(self) -> None:
  """Tests handling a characteristic read request.

  Test steps:
    1. Open a GATT server on DUT.
    2. Add a GATT service including a readable characteristic to the server
    instance.
    3. Read characteristic from REF.
    4. Handle the read request and send response from DUT.
    5. Check read result from REF.
  """
  # UUID must be random here, otherwise there might be interference when
  # multiple tests run in the same box.
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())

  self.logger.info("[DUT] Add a service.")
  await self.dut_gatt_server.add_service(
      bl4a_api.GattService(
          uuid=service_uuid,
          characteristics=[
              bl4a_api.GattCharacteristic(
                  uuid=characteristic_uuid,
                  properties=_Property.READ,
                  permissions=_Permission.READ,
              )
          ],
      ),
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self._make_le_connection()

  async with bumble.device.Peer(ref_dut_acl) as peer:
    characteristic = peer.get_characteristics_by_uuid(
        bumble.core.UUID(characteristic_uuid)
    )[0]

    self.logger.info("[REF] Read characteristic.")
    read_task = asyncio.create_task(characteristic.read_value())

    read_request = await self.dut_gatt_server.wait_for_event(
        event=bl4a_api.GattCharacteristicReadRequest,
        predicate=lambda request: (
            request.characteristic_uuid == characteristic_uuid
        ),
    )
    expected_data = secrets.token_bytes(16)
    self.dut_gatt_server.send_response(
        address=read_request.address,
        request_id=read_request.request_id,
        status=android_constants.GattStatus.SUCCESS,
        value=expected_data,
    )
    self.assertEqual(await read_task, expected_data)

Tests handling a characteristic write request.

Test steps
  1. Open a GATT server on DUT.
  2. Add a GATT service including a writable characteristic to the server instance.
  3. Write characteristic from REF.
  4. Handle the write request and send response from DUT.
  5. Check write result from REF.
Source code in navi/tests/smoke/gatt_server_test.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
async def test_handle_characteristic_write_request(self) -> None:
  """Tests handling a characteristic write request.

  Test steps:
    1. Open a GATT server on DUT.
    2. Add a GATT service including a writable characteristic to the server
    instance.
    3. Write characteristic from REF.
    4. Handle the write request and send response from DUT.
    5. Check write result from REF.
  """
  # UUID must be random here, otherwise there might be interference when
  # multiple tests run in the same box.
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())

  self.logger.info("[DUT] Add a service.")
  await self.dut_gatt_server.add_service(
      bl4a_api.GattService(
          uuid=service_uuid,
          characteristics=[
              bl4a_api.GattCharacteristic(
                  uuid=characteristic_uuid,
                  properties=_Property.WRITE,
                  permissions=_Permission.WRITE,
              )
          ],
      ),
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self._make_le_connection()

  async with bumble.device.Peer(ref_dut_acl) as peer:
    characteristic = peer.get_characteristics_by_uuid(
        bumble.core.UUID(characteristic_uuid)
    )[0]

    self.logger.info("[REF] Write characteristic.")
    expected_data = secrets.token_bytes(16)
    write_task = asyncio.create_task(
        characteristic.write_value(expected_data, with_response=True)
    )

    write_request = await self.dut_gatt_server.wait_for_event(
        event=bl4a_api.GattCharacteristicWriteRequest,
        predicate=lambda request: (
            request.characteristic_uuid == characteristic_uuid
        ),
    )
    self.assertEqual(write_request.value, expected_data)

    self.dut_gatt_server.send_response(
        address=write_request.address,
        request_id=write_request.request_id,
        status=android_constants.GattStatus.SUCCESS,
        value=b"",
    )
    await write_task

Tests sending GATT notification.

Test steps
  1. Add a GATT service including a characteristic to the server instance.
  2. Subscribe GATT characteristic from REF.
  3. Handle the subscribe request (CCCD write) from DUT.
  4. Send notification from DUT.
  5. Check notification from REF.
Source code in navi/tests/smoke/gatt_server_test.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
async def test_notify(self) -> None:
  """Tests sending GATT notification.

  Test steps:
    1. Add a GATT service including a characteristic to the server instance.
    2. Subscribe GATT characteristic from REF.
    3. Handle the subscribe request (CCCD write) from DUT.
    4. Send notification from DUT.
    5. Check notification from REF.
  """
  # UUID must be random here, otherwise there might be interference when
  # multiple tests run in the same box.
  service_uuid = str(uuid.uuid4())
  characteristic_uuid = str(uuid.uuid4())

  self.logger.info("[DUT] Add a service.")
  await self.dut_gatt_server.add_service(
      bl4a_api.GattService(
          uuid=service_uuid,
          characteristics=[
              bl4a_api.GattCharacteristic(
                  uuid=characteristic_uuid,
                  properties=_Property.READ | _Property.NOTIFY,
                  permissions=_Permission.READ,
                  descriptors=[
                      bl4a_api.GattDescriptor(
                          uuid=_CCCD_UUID,
                          permissions=_Permission.READ | _Permission.WRITE,
                      )
                  ],
              )
          ],
      ),
  )
  dut_characteristic = bl4a_api.find_characteristic_by_uuid(
      characteristic_uuid, self.dut_gatt_server.services
  )
  if not dut_characteristic.handle:
    self.fail("Cannot find characteristic.")

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self._make_le_connection()

  async with bumble.device.Peer(ref_dut_acl) as peer:
    ref_characteristic = peer.get_characteristics_by_uuid(
        bumble.core.UUID(characteristic_uuid)
    )[0]

    self.logger.info("[REF] Subscribe characteristic.")
    notification_queue = asyncio.Queue[bytes]()
    expected_data = secrets.token_bytes(16)
    subscribe_task = asyncio.create_task(
        ref_characteristic.subscribe(notification_queue.put_nowait)
    )

    self.logger.info("[DUT] Wait for CCCD write.")
    subscribe_request = await self.dut_gatt_server.wait_for_event(
        event=bl4a_api.GattDescriptorWriteRequest,
        predicate=lambda request: (
            request.characteristic_handle == dut_characteristic.handle
            and request.descriptor_uuid == _CCCD_UUID
        ),
    )

    self.logger.info("[DUT] Respond to CCCD write.")
    self.dut_gatt_server.send_response(
        address=subscribe_request.address,
        request_id=subscribe_request.request_id,
        status=android_constants.GattStatus.SUCCESS,
        value=b"",
    )

    self.logger.info("[REF] Wait subscription complete.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await subscribe_task

    self.logger.info("[DUT] Send notification.")
    self.dut_gatt_server.send_notification(
        address=self.ref.random_address,
        characteristic_handle=dut_characteristic.handle,
        confirm=False,
        value=expected_data,
    )

    self.logger.info("[REF] Wait for notification.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.assertEqual(await notification_queue.get(), expected_data)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/hfp_ag_test.py
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
class HfpAgTest(navi_test_base.TwoDevicesTestBase):

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.getprop(android_constants.Property.HFP_AG_ENABLED) != "true":
      raise signals.TestAbortClass("HFP(AG) is not enabled on DUT.")

  @override
  async def async_teardown_test(self) -> None:
    self.dut.bt.audioStop()
    # Make sure Bumble is off to cancel any running tasks.
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await self.ref.close()
    await super().async_teardown_test()

  def _is_ranchu_emulator(self, dev: android_device.AndroidDevice) -> bool:
    return (build_info := dev.build_info) and build_info["hardware"] == "ranchu"

  async def _wait_for_sco_state(
      self,
      dut_hfp_ag_callback: _CallbackHandler,
      state: _ScoState,
  ) -> None:
    await dut_hfp_ag_callback.wait_for_event(
        event=_HfpAgAudioStateChange(address=self.ref.address, state=state),
    )

  async def _wait_for_call_state(
      self,
      dut_telecom_callback: _CallbackHandler,
      *states,
  ) -> None:
    await dut_telecom_callback.wait_for_event(
        event=bl4a_api.CallStateChanged,
        predicate=lambda e: (e.state in states),
    )

  @classmethod
  def _default_hfp_configuration(cls) -> hfp.HfConfiguration:
    return hfp.HfConfiguration(
        supported_hf_features=[],
        supported_hf_indicators=[],
        supported_audio_codecs=[
            _AudioCodec.CVSD,
            _AudioCodec.MSBC,
        ],
    )

  async def _terminate_connection_from_dut(self) -> None:
    with (self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb,):
      self.logger.info("[DUT] Terminate connection.")
      self.dut.bt.disconnect(self.ref.address)
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  async def test_pair_and_connect(self) -> None:
    """Tests HFP connection establishment right after a pairing session.

    Test steps:
      1. Setup HFP on REF.
      2. Create bond from DUT.
      3. Wait HFP connected on DUT.(Android should autoconnect HFP as AG)
    """
    with (self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb,):
      hfp_ext.HfProtocol.setup_server(
          self.ref.device,
          sdp_handle=_HFP_SDP_HANDLE,
          configuration=self._default_hfp_configuration(),
      )

      self.logger.info("[DUT] Connect and pair REF.")
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for HFP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  async def test_paired_connect_outgoing(self) -> None:
    """Tests HFP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from DUT.
      4. Wait HFP connected on DUT.
      5. Disconnect from DUT.
      6. Wait HFP disconnected on DUT.
    """
    with (self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb,):
      await self.test_pair_and_connect()
      ref_address = self.ref.address

      await self._terminate_connection_from_dut()

      self.logger.info("[DUT] Reconnect.")
      self.dut.bt.connect(ref_address)

      self.logger.info("[DUT] Wait for HFP connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[DUT] Disconnect.")
      self.dut.bt.disconnect(ref_address)

      self.logger.info("[DUT] Wait for HFP disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=None),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  async def test_paired_connect_incoming(self) -> None:
    """Tests HFP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from REF.
      4. Wait HFP connected on DUT.
      5. Disconnect from REF.
      6. Wait HFP disconnected on DUT.
    """
    dut_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
    self.test_case_context.push(dut_cb)
    await self.test_pair_and_connect()

    await self._terminate_connection_from_dut()

    self.logger.info("[REF] Reconnect.")
    dut_ref_acl = await self.ref.device.connect(
        self.dut.address,
        core.BT_BR_EDR_TRANSPORT,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[REF] Authenticate and encrypt connection.")
    await dut_ref_acl.authenticate()
    await dut_ref_acl.encrypt()

    rfcomm_channel = await rfcomm.find_rfcomm_channel_with_uuid(
        dut_ref_acl, core.BT_HANDSFREE_AUDIO_GATEWAY_SERVICE
    )
    if rfcomm_channel is None:
      self.fail("No HFP RFCOMM channel found on REF.")
    self.logger.info("[REF] Found HFP RFCOMM channel %s.", rfcomm_channel)

    self.logger.info("[REF] Open RFCOMM Multiplexer.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      multiplexer = await rfcomm.Client(dut_ref_acl).start()

    self.logger.info("[REF] Open RFCOMM DLC.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      dlc = await multiplexer.open_dlc(rfcomm_channel)

    self.logger.info("[REF] Establish SLC.")
    ref_hfp_protocol = hfp_ext.HfProtocol(
        dlc, self._default_hfp_configuration()
    )
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_hfp_protocol.initiate_slc()

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[REF] Disconnect.")
    await dut_ref_acl.disconnect()

    self.logger.info("[DUT] Wait for HFP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=None),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

  @navi_test_base.named_parameterized(
      cvsd_only=dict(
          supported_audio_codecs=[
              _AudioCodec.CVSD,
          ]
      ),
      cvsd_msbc=dict(
          supported_audio_codecs=[
              _AudioCodec.CVSD,
              _AudioCodec.MSBC,
          ]
      ),
      cvsd_msbc_lc3_swb=dict(
          supported_audio_codecs=[
              _AudioCodec.CVSD,
              _AudioCodec.MSBC,
              _AudioCodec.LC3_SWB,
          ]
      ),
  )
  async def test_call_sco_connection_with_codec_negotiation(
      self,
      supported_audio_codecs: list[hfp.AudioCodec],
  ) -> None:
    """Tests making an outgoing phone call, observing SCO connection status.

    Test steps:
      1. Setup HFP connection.
      2. Place an outgoing call.
      3. Verify SCO connected.
      4. Terminate the call.
      5. Verify SCO disconnected.

    Args:
      supported_audio_codecs: Audio codecs supported by REF device.
    """

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[hfp.HfFeature.CODEC_NEGOTIATION],
        supported_hf_indicators=[],
        supported_audio_codecs=supported_audio_codecs,
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    if (
        _AudioCodec.LC3_SWB in supported_audio_codecs
        and self.dut.getprop(_PROPERTY_SWB_SUPPORTED) == "true"
    ):
      preferred_codec = _AudioCodec.LC3_SWB
      # Sample rate is defined in HFP 1.9 spec.
      sample_rate = 32000
    elif _AudioCodec.MSBC in supported_audio_codecs:
      preferred_codec = _AudioCodec.MSBC
      sample_rate = 16000
    else:
      preferred_codec = _AudioCodec.CVSD
      sample_rate = 8000
    # PCM frame size = sample_rate * frame_duration (7.5ms) * sample_width (2)
    pcm_frame_size = int(sample_rate * _HFP_FRAME_DURATION * 2)

    dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
    dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
    self.test_case_context.push(dut_hfp_cb)
    self.test_case_context.push(dut_telecom_cb)

    self.logger.info("[DUT] Connect and pair REF.")
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    sco_links = asyncio.Queue[device.ScoLink]()
    self.ref.device.on(
        self.ref.device.EVENT_SCO_CONNECTION, sco_links.put_nowait
    )

    self.logger.info("[DUT] Add call.")
    with self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    ) as call:
      await self._wait_for_call_state(
          dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
      )

      self.logger.info("[DUT] Start streaming.")
      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
      await asyncio.to_thread(self.dut.bt.audioPlaySine)

      self.logger.info("[DUT] Wait for SCO connected.")
      await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)

      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[REF] Wait for SCO connected.")
        sco_link = await sco_links.get()

        self.assertEqual(ref_hfp_protocol.active_codec, preferred_codec)

      self.logger.info("[DUT] Start recording.")
      recorder = await asyncio.to_thread(
          lambda: self.dut.bl4a.start_audio_recording(_RECORDING_PATH)
      )
      # Make sure the recorder is closed after the test.
      self.test_case_context.push(recorder)

      esco_parameters = await ref_hfp_protocol.get_esco_parameters()
      check_audio_correctness = (
          # We don't support transparent audio packets for now.
          esco_parameters.input_coding_format.codec_id == hci.CodecID.LINEAR_PCM
          # Skip audio correctness check on emulators.
          and not self.dut.device.is_emulator
          and audio.SUPPORT_AUDIO_PROCESSING
      )
      ref_sink_buffer = bytearray()
      if check_audio_correctness:
        sine_tone_batch_iterator = itertools.cycle(
            audio.batched(
                audio.generate_sine_tone(
                    frequency=1000,
                    duration=1.0,
                    sample_rate=sample_rate,
                    data_type="int16",
                ),
                n=pcm_frame_size,
            )
        )

        async def source_streamer() -> None:
          while sco_link.handle in self.ref.device.sco_links:
            tx_data = next(sine_tone_batch_iterator)
            for offset in range(0, len(tx_data), _MAX_FRAME_SIZE):
              buffer = tx_data[offset : offset + _MAX_FRAME_SIZE]
              self.ref.device.host.send_hci_packet(
                  hci.HCI_SynchronousDataPacket(
                      connection_handle=sco_link.handle,
                      packet_status=0,
                      data_total_length=len(buffer),
                      data=bytes(buffer),
                  )
              )
            # Sleep for 90% of the frame duration, or packets might be dropped.
            await asyncio.sleep(_HFP_FRAME_DURATION * 0.9)

        def on_sco_packet(packet: hci.HCI_SynchronousDataPacket) -> None:
          ref_sink_buffer.extend(packet.data)

        sco_link.sink = on_sco_packet
        sco_link.abort_on(sco_link.EVENT_DISCONNECTION, source_streamer())

      # Streaming for 5 seconds.
      await asyncio.sleep(5.0)

      self.logger.info("[DUT] Terminate call.")
      call.close()
      await self._wait_for_call_state(dut_telecom_cb, _CallState.DISCONNECTED)

    self.logger.info("[DUT] Wait for SCO disconnected.")
    await self._wait_for_sco_state(dut_hfp_cb, _ScoState.DISCONNECTED)

    self.logger.info("[DUT] Stop recording.")
    await asyncio.to_thread(recorder.close)

    # Get recording from DUT.
    rx_received_buffer = self.dut.adb.shell([
        "cat",
        f"/data/media/{self.dut.adb.current_user_id}/Recordings/record.wav",
    ])

    if (
        self.user_params.get(navi_test_base.RECORD_FULL_DATA)
        and rx_received_buffer
    ):
      self.write_test_output_data(
          f"hfp_ag_data.{preferred_codec.name.lower()}",
          rx_received_buffer,
      )

    if check_audio_correctness:
      tx_dominant_frequency = audio.get_dominant_frequency(
          ref_sink_buffer,
          format="pcm",
          frame_rate=sample_rate,
          channels=1,
          sample_width=2,  # 16-bit
      )
      self.logger.info("[Tx] Dominant frequency: %.2f", tx_dominant_frequency)
      self.assertAlmostEqual(tx_dominant_frequency, 1000, delta=10)
      rx_dominant_frequency = audio.get_dominant_frequency(
          rx_received_buffer, format="wav"
      )
      self.logger.info("[Rx] Dominant frequency: %.2f", rx_dominant_frequency)
      self.assertAlmostEqual(rx_dominant_frequency, 1000, delta=10)

  @navi_test_base.parameterized(_CallAnswer.ACCEPT, _CallAnswer.REJECT)
  async def test_answer_call_from_ref(self, call_answer: _CallAnswer) -> None:
    """Tests answering an incoming phone call from REF.

    Test steps:
      1. Setup HFP connection.
      2. Place an incoming call.
      3. Answer call on REF.
      4. Verify call status.

    Args:
      call_answer: Answer type of call.
    """
    if self._is_ranchu_emulator(self.dut.device):
      self.skipTest("Call control is not supported on Ranchu emulator")

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
    dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
    self.test_case_context.push(dut_hfp_cb)
    self.test_case_context.push(dut_telecom_cb)

    self.logger.info("[DUT] Connect and pair REF.")
    await self.classic_connect_and_pair()

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    condition = asyncio.Condition()

    @ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR)
    async def _(*_) -> None:
      async with condition:
        condition.notify_all()

    self.logger.info("[DUT] Make incoming call.")
    with self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.INCOMING,
    ):
      await self._wait_for_call_state(dut_telecom_cb, _CallState.RINGING)

      self.logger.info("[REF] Wait for callsetup.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        async with condition:
          call_setup = next(
              indicator
              for indicator in ref_hfp_protocol.ag_indicators
              if indicator.indicator == hfp.AgIndicator.CALL_SETUP
          )
          await condition.wait_for(lambda: (call_setup.current_status == 1))

      if call_answer == _CallAnswer.ACCEPT:
        self.logger.info("[REF] Answer call.")
        await ref_hfp_protocol.answer_incoming_call()
        await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)
      else:
        self.logger.info("[REF] Reject call.")
        await ref_hfp_protocol.reject_incoming_call()
        await self._wait_for_call_state(dut_telecom_cb, _CallState.DISCONNECTED)

  @navi_test_base.parameterized(
      constants.Direction.INCOMING,
      constants.Direction.OUTGOING,
  )
  async def test_callsetup_ag_indicator(
      self,
      direction: constants.Direction,
  ) -> None:
    """Tests making phone call, observing AG indicator.

    Test steps:
      1. Setup HFP connection.
      2. Place a phone call.
      3. Verify callsetup ag indicator.
      4. Answer the call
      5. Verify callsetup and call ag indicator.
      6. Terminate the call.
      7. Verify call ag indicator.

    Args:
      direction: The direction of phone call.
    """

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    self.logger.info("[DUT] Connect and pair REF.")
    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for HFP connected.")
      await dut_hfp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    ag_indicators = collections.defaultdict[
        hfp.AgIndicator, asyncio.Queue[int]
    ](asyncio.Queue)

    def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
      ag_indicators[ag_indicator.indicator].put_nowait(
          ag_indicator.current_status
      )

    ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

    self.logger.info("[DUT] Make phone call.")
    with self.dut.bl4a.make_phone_call(
        _CALLER_NAME, _CALLER_NUMBER, direction
    ) as call:
      if direction == constants.Direction.INCOMING:
        self.logger.info("[REF] Wait for (callsetup, 1 - incoming).")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.assertEqual(
              await ag_indicators[_AgIndicator.CALL_SETUP].get(),
              hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
          )
      else:
        self.logger.info("[REF] Wait for (callsetup, 2 - outgoing).")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.assertEqual(
              await ag_indicators[_AgIndicator.CALL_SETUP].get(),
              hfp.CallSetupAgIndicator.OUTGOING_CALL_SETUP,
          )
        self.logger.info("[REF] Wait for (callsetup, 3 - remote alerted).")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.assertEqual(
              await ag_indicators[_AgIndicator.CALL_SETUP].get(),
              hfp.CallSetupAgIndicator.REMOTE_ALERTED,
          )

      self.logger.info("[DUT] Answer Call.")
      call.answer()

      self.logger.info("[REF] Wait for (callsetup, 0 - not in setup).")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(
            await ag_indicators[_AgIndicator.CALL_SETUP].get(),
            hfp.CallSetupAgIndicator.NOT_IN_CALL_SETUP,
        )

      self.logger.info("[REF] Wait for (call, 1 - active).")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(
            await ag_indicators[_AgIndicator.CALL].get(),
            _CallAgIndicator.ACTIVE,
        )

    self.logger.info("[REF] Wait for (call, 0 - inactive).")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.assertEqual(
          await ag_indicators[_AgIndicator.CALL].get(),
          _CallAgIndicator.INACTIVE,
      )

  async def test_update_battery_level(self) -> None:
    """Tests updating battery level indicator from HF.

    Test steps:
      1. Setup HFP connection.
      2. Send battery level indicator from HF.
      3. Verify call ag indicator.
    """

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[hfp.HfFeature.HF_INDICATORS],
        supported_hf_indicators=[hfp.HfIndicator.BATTERY_LEVEL],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    with (
        self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb,
        self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_adapter_cb,
    ):
      await self.classic_connect_and_pair()
      self.logger.info("[DUT] Wait for HFP connected.")
      await dut_hfp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for HFP connected.",
      ):
        ref_hfp_protocol = await ref_hfp_protocol_queue.get()

      if not ref_hfp_protocol.supports_ag_feature(hfp.AgFeature.HF_INDICATORS):
        raise signals.TestSkip("DUT doesn't support HF Indicator")

      for i in range(101):
        await ref_hfp_protocol.execute_command(
            f"AT+BIEV={hfp.HfIndicator.BATTERY_LEVEL.value},{i}"
        )
        event = await dut_adapter_cb.wait_for_event(
            bl4a_api.BatteryLevelChanged,
            predicate=lambda e: (e.address == self.ref.address),
        )
        self.assertEqual(event.level, i)

  async def test_connect_hf_during_call_should_route_to_hf(self) -> None:
    """Tests connecting HFP during phone call should route to HFP.

    Test steps:
      1. Place a call.
      2. Setup HFP connection.
    """

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    self.logger.info("[DUT] Make outgoing call.")
    with (
        self.dut.bl4a.register_callback(_Module.TELECOM) as dut_telecom_cb,
        self.dut.bl4a.make_phone_call(
            _CALLER_NAME,
            _CALLER_NUMBER,
            constants.Direction.OUTGOING,
        ),
    ):
      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
      self.dut.bt.audioPlaySine()

      await self._wait_for_call_state(
          dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
      )

      self.logger.info("[DUT] Connect and pair REF.")
      with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
        await self.classic_connect_and_pair()

        self.logger.info("[DUT] Wait for SCO connected.")
        await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)

  @navi_test_base.parameterized(constants.TestRole.DUT, constants.TestRole.REF)
  @navi_test_base.retry(max_count=2)
  async def test_adjust_speaker_volume(
      self, issuer: constants.TestRole
  ) -> None:
    """Tests adjusting speaker volume with HFP.

    Test steps:
      1. Place a call.
      2. Setup HFP connection.
      3. Adjust volume.

    Args:
      issuer: The issuer of volume adjustment.
    """
    if self._is_ranchu_emulator(self.dut.device):
      self.skipTest("Volume control is not supported on Ranchu emulator")
    if self.dut.device.is_emulator and issuer == constants.TestRole.DUT:
      self.skipTest("b/420835576: Volume control from DUT is broken")

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[hfp.HfFeature.REMOTE_VOLUME_CONTROL],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    self.logger.info("[DUT] Connect and pair REF.")
    with (
        self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb,
        self.dut.bl4a.register_callback(_Module.AUDIO) as dut_audio_cb,
        self.dut.bl4a.make_phone_call(
            _CALLER_NAME,
            _CALLER_NUMBER,
            constants.Direction.OUTGOING,
        ),
    ):
      await self.classic_connect_and_pair()

      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
      self.dut.bt.audioPlaySine()

      self.logger.info("[DUT] Wait for SCO connected.")
      await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for HFP connected.",
      ):
        ref_hfp_protocol = await ref_hfp_protocol_queue.get()

      if not self.dut.device.is_emulator:
        self.logger.info("[DUT] Wait for SCO active.")
        await dut_audio_cb.wait_for_event(
            bl4a_api.CommunicationDeviceChanged(
                self.ref.address,
                device_type=android_constants.AudioDeviceType.BLUETOOTH_SCO,
            )
        )

      # Somehow volume change cannot be broadcasted to Bluetooth at the moment
      # when SCO becomes active.
      await asyncio.sleep(0.5)

      for expected_volume in range(1, _HFP_MAX_VOLUME + 1):
        if expected_volume == self.dut.bt.getVolume(_STREAM_TYPE_CALL):
          continue

        if issuer == constants.TestRole.DUT:
          volumes = asyncio.Queue[int]()
          ref_hfp_protocol.on(
              ref_hfp_protocol.EVENT_SPEAKER_VOLUME, volumes.put_nowait
          )

          self.logger.info("[DUT] Set volume to %d.", expected_volume)
          self.dut.bt.setVolume(_STREAM_TYPE_CALL, expected_volume)

          self.logger.info("[REF] Wait for volume changed event.")
          async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
            actual_volume = await volumes.get()
          self.assertEqual(actual_volume, expected_volume)
        else:
          self.logger.info("[REF] Set volume to %d.", expected_volume)
          await ref_hfp_protocol.execute_command(f"AT+VGS={expected_volume}")

          self.logger.info("[DUT] Wait for volume changed event.")
          await dut_audio_cb.wait_for_event(
              event=bl4a_api.VolumeChanged(
                  stream_type=_STREAM_TYPE_CALL, volume_value=expected_volume
              ),
          )

  async def test_query_call_status(self) -> None:
    """Tests querying call status from HF.

    Test steps:
      1. Setup HFP connection.
      2. Place a call.
      3. Query call status from HF.
      4. Terminate the call.
      5. Query call status from HF.
    """

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    self.logger.info("[DUT] Connect and pair REF.")
    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
      await self.classic_connect_and_pair()
      await dut_hfp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for HFP connected.",
      ):
        ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    ag_indicators = collections.defaultdict[
        hfp.AgIndicator, asyncio.Queue[int]
    ](asyncio.Queue)

    def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
      ag_indicators[ag_indicator.indicator].put_nowait(
          ag_indicator.current_status
      )

    ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

    self.logger.info("[DUT] Make incoming call.")
    with self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.INCOMING,
    ):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        call_setup_state = await ag_indicators[_AgIndicator.CALL_SETUP].get()
        self.assertEqual(call_setup_state, 1)

      calls = await ref_hfp_protocol.query_current_calls()
      self.assertLen(calls, 1)
      self.assertEqual(
          calls[0].direction,
          hfp.CallInfoDirection.MOBILE_TERMINATED_CALL,
      )
      self.assertEqual(calls[0].status, hfp.CallInfoStatus.INCOMING)
      self.assertEqual(calls[0].number, _CALLER_NUMBER)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      call_setup_state = await ag_indicators[_AgIndicator.CALL_SETUP].get()
      self.assertEqual(call_setup_state, 0)

    calls = await ref_hfp_protocol.query_current_calls()
    self.assertEmpty(calls)

  async def test_hold_unhold_call(self) -> None:
    """Tests holding and unholding call with HFP.

    Test steps:
      1. Setup HFP connection.
      2. Place an outgoing call.
      3. Hold the call.
      4. Unhold the call.
    """
    if self._is_ranchu_emulator(self.dut.device):
      self.skipTest("Call hold is not supported on Ranchu emulator")

    # [REF] Setup HFP.
    hfp_configuration = hfp.HfConfiguration(
        supported_hf_features=[hfp.HfFeature.THREE_WAY_CALLING],
        supported_hf_indicators=[],
        supported_audio_codecs=[hfp.AudioCodec.CVSD],
    )
    ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=hfp_configuration,
    )

    self.logger.info("[DUT] Connect and pair REF.")
    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
      await self.classic_connect_and_pair()
      await dut_hfp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for HFP connected.",
      ):
        ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    ag_indicators = collections.defaultdict[
        hfp.AgIndicator, asyncio.Queue[int]
    ](asyncio.Queue)

    def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
      ag_indicators[ag_indicator.indicator].put_nowait(
          ag_indicator.current_status
      )

    ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

    self.logger.info("[DUT] Make incoming call.")
    with (
        self.dut.bl4a.register_callback(_Module.TELECOM) as dut_telecom_cb,
        self.dut.bl4a.make_phone_call(
            _CALLER_NAME,
            _CALLER_NUMBER,
            constants.Direction.OUTGOING,
        ) as call,
    ):
      # 25Q1 => CONNECTING, 25Q2 -> DIALING
      await self._wait_for_call_state(
          dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
      )
      call.answer()
      await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)

      self.logger.info("[REF] Hold call.")
      await ref_hfp_protocol.execute_command("AT+CHLD=2")

      self.logger.info("[DUT] Wait for call state to be HOLDING.")
      await self._wait_for_call_state(dut_telecom_cb, _CallState.HOLDING)

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call state to be HOLDING.",
      ):
        call_setup_state = await ag_indicators[_AgIndicator.CALL_HELD].get()
        self.assertEqual(
            call_setup_state,
            hfp.CallHeldAgIndicator.CALL_ON_HOLD_NO_ACTIVE_CALL,
        )

      self.logger.info("[REF] Unhold call.")
      await ref_hfp_protocol.execute_command("AT+CHLD=2")

      self.logger.info("[DUT] Wait for call state to be ACTIVE.")
      await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call state to be NO_CALLS_HELD.",
      ):
        call_setup_state = await ag_indicators[_AgIndicator.CALL_HELD].get()
        self.assertEqual(
            call_setup_state, hfp.CallHeldAgIndicator.NO_CALLS_HELD
        )

Tests adjusting speaker volume with HFP.

Test steps
  1. Place a call.
  2. Setup HFP connection.
  3. Adjust volume.

Parameters:

Name Type Description Default
issuer TestRole

The issuer of volume adjustment.

required
Source code in navi/tests/smoke/hfp_ag_test.py
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
@navi_test_base.parameterized(constants.TestRole.DUT, constants.TestRole.REF)
@navi_test_base.retry(max_count=2)
async def test_adjust_speaker_volume(
    self, issuer: constants.TestRole
) -> None:
  """Tests adjusting speaker volume with HFP.

  Test steps:
    1. Place a call.
    2. Setup HFP connection.
    3. Adjust volume.

  Args:
    issuer: The issuer of volume adjustment.
  """
  if self._is_ranchu_emulator(self.dut.device):
    self.skipTest("Volume control is not supported on Ranchu emulator")
  if self.dut.device.is_emulator and issuer == constants.TestRole.DUT:
    self.skipTest("b/420835576: Volume control from DUT is broken")

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[hfp.HfFeature.REMOTE_VOLUME_CONTROL],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  self.logger.info("[DUT] Connect and pair REF.")
  with (
      self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb,
      self.dut.bl4a.register_callback(_Module.AUDIO) as dut_audio_cb,
      self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          constants.Direction.OUTGOING,
      ),
  ):
    await self.classic_connect_and_pair()

    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    self.dut.bt.audioPlaySine()

    self.logger.info("[DUT] Wait for SCO connected.")
    await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    if not self.dut.device.is_emulator:
      self.logger.info("[DUT] Wait for SCO active.")
      await dut_audio_cb.wait_for_event(
          bl4a_api.CommunicationDeviceChanged(
              self.ref.address,
              device_type=android_constants.AudioDeviceType.BLUETOOTH_SCO,
          )
      )

    # Somehow volume change cannot be broadcasted to Bluetooth at the moment
    # when SCO becomes active.
    await asyncio.sleep(0.5)

    for expected_volume in range(1, _HFP_MAX_VOLUME + 1):
      if expected_volume == self.dut.bt.getVolume(_STREAM_TYPE_CALL):
        continue

      if issuer == constants.TestRole.DUT:
        volumes = asyncio.Queue[int]()
        ref_hfp_protocol.on(
            ref_hfp_protocol.EVENT_SPEAKER_VOLUME, volumes.put_nowait
        )

        self.logger.info("[DUT] Set volume to %d.", expected_volume)
        self.dut.bt.setVolume(_STREAM_TYPE_CALL, expected_volume)

        self.logger.info("[REF] Wait for volume changed event.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          actual_volume = await volumes.get()
        self.assertEqual(actual_volume, expected_volume)
      else:
        self.logger.info("[REF] Set volume to %d.", expected_volume)
        await ref_hfp_protocol.execute_command(f"AT+VGS={expected_volume}")

        self.logger.info("[DUT] Wait for volume changed event.")
        await dut_audio_cb.wait_for_event(
            event=bl4a_api.VolumeChanged(
                stream_type=_STREAM_TYPE_CALL, volume_value=expected_volume
            ),
        )

Tests answering an incoming phone call from REF.

Test steps
  1. Setup HFP connection.
  2. Place an incoming call.
  3. Answer call on REF.
  4. Verify call status.

Parameters:

Name Type Description Default
call_answer _CallAnswer

Answer type of call.

required
Source code in navi/tests/smoke/hfp_ag_test.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
@navi_test_base.parameterized(_CallAnswer.ACCEPT, _CallAnswer.REJECT)
async def test_answer_call_from_ref(self, call_answer: _CallAnswer) -> None:
  """Tests answering an incoming phone call from REF.

  Test steps:
    1. Setup HFP connection.
    2. Place an incoming call.
    3. Answer call on REF.
    4. Verify call status.

  Args:
    call_answer: Answer type of call.
  """
  if self._is_ranchu_emulator(self.dut.device):
    self.skipTest("Call control is not supported on Ranchu emulator")

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
  dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
  self.test_case_context.push(dut_hfp_cb)
  self.test_case_context.push(dut_telecom_cb)

  self.logger.info("[DUT] Connect and pair REF.")
  await self.classic_connect_and_pair()

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for HFP connected.",
  ):
    ref_hfp_protocol = await ref_hfp_protocol_queue.get()

  self.logger.info("[DUT] Wait for HFP connected.")
  await dut_hfp_cb.wait_for_event(
      bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

  condition = asyncio.Condition()

  @ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR)
  async def _(*_) -> None:
    async with condition:
      condition.notify_all()

  self.logger.info("[DUT] Make incoming call.")
  with self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.INCOMING,
  ):
    await self._wait_for_call_state(dut_telecom_cb, _CallState.RINGING)

    self.logger.info("[REF] Wait for callsetup.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      async with condition:
        call_setup = next(
            indicator
            for indicator in ref_hfp_protocol.ag_indicators
            if indicator.indicator == hfp.AgIndicator.CALL_SETUP
        )
        await condition.wait_for(lambda: (call_setup.current_status == 1))

    if call_answer == _CallAnswer.ACCEPT:
      self.logger.info("[REF] Answer call.")
      await ref_hfp_protocol.answer_incoming_call()
      await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)
    else:
      self.logger.info("[REF] Reject call.")
      await ref_hfp_protocol.reject_incoming_call()
      await self._wait_for_call_state(dut_telecom_cb, _CallState.DISCONNECTED)

Tests making an outgoing phone call, observing SCO connection status.

Test steps
  1. Setup HFP connection.
  2. Place an outgoing call.
  3. Verify SCO connected.
  4. Terminate the call.
  5. Verify SCO disconnected.

Parameters:

Name Type Description Default
supported_audio_codecs list[AudioCodec]

Audio codecs supported by REF device.

required
Source code in navi/tests/smoke/hfp_ag_test.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
@navi_test_base.named_parameterized(
    cvsd_only=dict(
        supported_audio_codecs=[
            _AudioCodec.CVSD,
        ]
    ),
    cvsd_msbc=dict(
        supported_audio_codecs=[
            _AudioCodec.CVSD,
            _AudioCodec.MSBC,
        ]
    ),
    cvsd_msbc_lc3_swb=dict(
        supported_audio_codecs=[
            _AudioCodec.CVSD,
            _AudioCodec.MSBC,
            _AudioCodec.LC3_SWB,
        ]
    ),
)
async def test_call_sco_connection_with_codec_negotiation(
    self,
    supported_audio_codecs: list[hfp.AudioCodec],
) -> None:
  """Tests making an outgoing phone call, observing SCO connection status.

  Test steps:
    1. Setup HFP connection.
    2. Place an outgoing call.
    3. Verify SCO connected.
    4. Terminate the call.
    5. Verify SCO disconnected.

  Args:
    supported_audio_codecs: Audio codecs supported by REF device.
  """

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[hfp.HfFeature.CODEC_NEGOTIATION],
      supported_hf_indicators=[],
      supported_audio_codecs=supported_audio_codecs,
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  if (
      _AudioCodec.LC3_SWB in supported_audio_codecs
      and self.dut.getprop(_PROPERTY_SWB_SUPPORTED) == "true"
  ):
    preferred_codec = _AudioCodec.LC3_SWB
    # Sample rate is defined in HFP 1.9 spec.
    sample_rate = 32000
  elif _AudioCodec.MSBC in supported_audio_codecs:
    preferred_codec = _AudioCodec.MSBC
    sample_rate = 16000
  else:
    preferred_codec = _AudioCodec.CVSD
    sample_rate = 8000
  # PCM frame size = sample_rate * frame_duration (7.5ms) * sample_width (2)
  pcm_frame_size = int(sample_rate * _HFP_FRAME_DURATION * 2)

  dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
  dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
  self.test_case_context.push(dut_hfp_cb)
  self.test_case_context.push(dut_telecom_cb)

  self.logger.info("[DUT] Connect and pair REF.")
  await self.classic_connect_and_pair()

  self.logger.info("[DUT] Wait for HFP connected.")
  await dut_hfp_cb.wait_for_event(
      bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for HFP connected.",
  ):
    ref_hfp_protocol = await ref_hfp_protocol_queue.get()

  sco_links = asyncio.Queue[device.ScoLink]()
  self.ref.device.on(
      self.ref.device.EVENT_SCO_CONNECTION, sco_links.put_nowait
  )

  self.logger.info("[DUT] Add call.")
  with self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.OUTGOING,
  ) as call:
    await self._wait_for_call_state(
        dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
    )

    self.logger.info("[DUT] Start streaming.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    await asyncio.to_thread(self.dut.bt.audioPlaySine)

    self.logger.info("[DUT] Wait for SCO connected.")
    await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for SCO connected.")
      sco_link = await sco_links.get()

      self.assertEqual(ref_hfp_protocol.active_codec, preferred_codec)

    self.logger.info("[DUT] Start recording.")
    recorder = await asyncio.to_thread(
        lambda: self.dut.bl4a.start_audio_recording(_RECORDING_PATH)
    )
    # Make sure the recorder is closed after the test.
    self.test_case_context.push(recorder)

    esco_parameters = await ref_hfp_protocol.get_esco_parameters()
    check_audio_correctness = (
        # We don't support transparent audio packets for now.
        esco_parameters.input_coding_format.codec_id == hci.CodecID.LINEAR_PCM
        # Skip audio correctness check on emulators.
        and not self.dut.device.is_emulator
        and audio.SUPPORT_AUDIO_PROCESSING
    )
    ref_sink_buffer = bytearray()
    if check_audio_correctness:
      sine_tone_batch_iterator = itertools.cycle(
          audio.batched(
              audio.generate_sine_tone(
                  frequency=1000,
                  duration=1.0,
                  sample_rate=sample_rate,
                  data_type="int16",
              ),
              n=pcm_frame_size,
          )
      )

      async def source_streamer() -> None:
        while sco_link.handle in self.ref.device.sco_links:
          tx_data = next(sine_tone_batch_iterator)
          for offset in range(0, len(tx_data), _MAX_FRAME_SIZE):
            buffer = tx_data[offset : offset + _MAX_FRAME_SIZE]
            self.ref.device.host.send_hci_packet(
                hci.HCI_SynchronousDataPacket(
                    connection_handle=sco_link.handle,
                    packet_status=0,
                    data_total_length=len(buffer),
                    data=bytes(buffer),
                )
            )
          # Sleep for 90% of the frame duration, or packets might be dropped.
          await asyncio.sleep(_HFP_FRAME_DURATION * 0.9)

      def on_sco_packet(packet: hci.HCI_SynchronousDataPacket) -> None:
        ref_sink_buffer.extend(packet.data)

      sco_link.sink = on_sco_packet
      sco_link.abort_on(sco_link.EVENT_DISCONNECTION, source_streamer())

    # Streaming for 5 seconds.
    await asyncio.sleep(5.0)

    self.logger.info("[DUT] Terminate call.")
    call.close()
    await self._wait_for_call_state(dut_telecom_cb, _CallState.DISCONNECTED)

  self.logger.info("[DUT] Wait for SCO disconnected.")
  await self._wait_for_sco_state(dut_hfp_cb, _ScoState.DISCONNECTED)

  self.logger.info("[DUT] Stop recording.")
  await asyncio.to_thread(recorder.close)

  # Get recording from DUT.
  rx_received_buffer = self.dut.adb.shell([
      "cat",
      f"/data/media/{self.dut.adb.current_user_id}/Recordings/record.wav",
  ])

  if (
      self.user_params.get(navi_test_base.RECORD_FULL_DATA)
      and rx_received_buffer
  ):
    self.write_test_output_data(
        f"hfp_ag_data.{preferred_codec.name.lower()}",
        rx_received_buffer,
    )

  if check_audio_correctness:
    tx_dominant_frequency = audio.get_dominant_frequency(
        ref_sink_buffer,
        format="pcm",
        frame_rate=sample_rate,
        channels=1,
        sample_width=2,  # 16-bit
    )
    self.logger.info("[Tx] Dominant frequency: %.2f", tx_dominant_frequency)
    self.assertAlmostEqual(tx_dominant_frequency, 1000, delta=10)
    rx_dominant_frequency = audio.get_dominant_frequency(
        rx_received_buffer, format="wav"
    )
    self.logger.info("[Rx] Dominant frequency: %.2f", rx_dominant_frequency)
    self.assertAlmostEqual(rx_dominant_frequency, 1000, delta=10)

Tests making phone call, observing AG indicator.

Test steps
  1. Setup HFP connection.
  2. Place a phone call.
  3. Verify callsetup ag indicator.
  4. Answer the call
  5. Verify callsetup and call ag indicator.
  6. Terminate the call.
  7. Verify call ag indicator.

Parameters:

Name Type Description Default
direction Direction

The direction of phone call.

required
Source code in navi/tests/smoke/hfp_ag_test.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
@navi_test_base.parameterized(
    constants.Direction.INCOMING,
    constants.Direction.OUTGOING,
)
async def test_callsetup_ag_indicator(
    self,
    direction: constants.Direction,
) -> None:
  """Tests making phone call, observing AG indicator.

  Test steps:
    1. Setup HFP connection.
    2. Place a phone call.
    3. Verify callsetup ag indicator.
    4. Answer the call
    5. Verify callsetup and call ag indicator.
    6. Terminate the call.
    7. Verify call ag indicator.

  Args:
    direction: The direction of phone call.
  """

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  self.logger.info("[DUT] Connect and pair REF.")
  with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for HFP connected.",
  ):
    ref_hfp_protocol = await ref_hfp_protocol_queue.get()

  ag_indicators = collections.defaultdict[
      hfp.AgIndicator, asyncio.Queue[int]
  ](asyncio.Queue)

  def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
    ag_indicators[ag_indicator.indicator].put_nowait(
        ag_indicator.current_status
    )

  ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

  self.logger.info("[DUT] Make phone call.")
  with self.dut.bl4a.make_phone_call(
      _CALLER_NAME, _CALLER_NUMBER, direction
  ) as call:
    if direction == constants.Direction.INCOMING:
      self.logger.info("[REF] Wait for (callsetup, 1 - incoming).")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(
            await ag_indicators[_AgIndicator.CALL_SETUP].get(),
            hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
        )
    else:
      self.logger.info("[REF] Wait for (callsetup, 2 - outgoing).")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(
            await ag_indicators[_AgIndicator.CALL_SETUP].get(),
            hfp.CallSetupAgIndicator.OUTGOING_CALL_SETUP,
        )
      self.logger.info("[REF] Wait for (callsetup, 3 - remote alerted).")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.assertEqual(
            await ag_indicators[_AgIndicator.CALL_SETUP].get(),
            hfp.CallSetupAgIndicator.REMOTE_ALERTED,
        )

    self.logger.info("[DUT] Answer Call.")
    call.answer()

    self.logger.info("[REF] Wait for (callsetup, 0 - not in setup).")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.assertEqual(
          await ag_indicators[_AgIndicator.CALL_SETUP].get(),
          hfp.CallSetupAgIndicator.NOT_IN_CALL_SETUP,
      )

    self.logger.info("[REF] Wait for (call, 1 - active).")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.assertEqual(
          await ag_indicators[_AgIndicator.CALL].get(),
          _CallAgIndicator.ACTIVE,
      )

  self.logger.info("[REF] Wait for (call, 0 - inactive).")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.assertEqual(
        await ag_indicators[_AgIndicator.CALL].get(),
        _CallAgIndicator.INACTIVE,
    )

Tests connecting HFP during phone call should route to HFP.

Test steps
  1. Place a call.
  2. Setup HFP connection.
Source code in navi/tests/smoke/hfp_ag_test.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
async def test_connect_hf_during_call_should_route_to_hf(self) -> None:
  """Tests connecting HFP during phone call should route to HFP.

  Test steps:
    1. Place a call.
    2. Setup HFP connection.
  """

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  self.logger.info("[DUT] Make outgoing call.")
  with (
      self.dut.bl4a.register_callback(_Module.TELECOM) as dut_telecom_cb,
      self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          constants.Direction.OUTGOING,
      ),
  ):
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    self.dut.bt.audioPlaySine()

    await self._wait_for_call_state(
        dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
    )

    self.logger.info("[DUT] Connect and pair REF.")
    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for SCO connected.")
      await self._wait_for_sco_state(dut_hfp_cb, _ScoState.CONNECTED)

Tests holding and unholding call with HFP.

Test steps
  1. Setup HFP connection.
  2. Place an outgoing call.
  3. Hold the call.
  4. Unhold the call.
Source code in navi/tests/smoke/hfp_ag_test.py
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
async def test_hold_unhold_call(self) -> None:
  """Tests holding and unholding call with HFP.

  Test steps:
    1. Setup HFP connection.
    2. Place an outgoing call.
    3. Hold the call.
    4. Unhold the call.
  """
  if self._is_ranchu_emulator(self.dut.device):
    self.skipTest("Call hold is not supported on Ranchu emulator")

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[hfp.HfFeature.THREE_WAY_CALLING],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  self.logger.info("[DUT] Connect and pair REF.")
  with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
    await self.classic_connect_and_pair()
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

  ag_indicators = collections.defaultdict[
      hfp.AgIndicator, asyncio.Queue[int]
  ](asyncio.Queue)

  def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
    ag_indicators[ag_indicator.indicator].put_nowait(
        ag_indicator.current_status
    )

  ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

  self.logger.info("[DUT] Make incoming call.")
  with (
      self.dut.bl4a.register_callback(_Module.TELECOM) as dut_telecom_cb,
      self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          constants.Direction.OUTGOING,
      ) as call,
  ):
    # 25Q1 => CONNECTING, 25Q2 -> DIALING
    await self._wait_for_call_state(
        dut_telecom_cb, _CallState.CONNECTING, _CallState.DIALING
    )
    call.answer()
    await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)

    self.logger.info("[REF] Hold call.")
    await ref_hfp_protocol.execute_command("AT+CHLD=2")

    self.logger.info("[DUT] Wait for call state to be HOLDING.")
    await self._wait_for_call_state(dut_telecom_cb, _CallState.HOLDING)

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call state to be HOLDING.",
    ):
      call_setup_state = await ag_indicators[_AgIndicator.CALL_HELD].get()
      self.assertEqual(
          call_setup_state,
          hfp.CallHeldAgIndicator.CALL_ON_HOLD_NO_ACTIVE_CALL,
      )

    self.logger.info("[REF] Unhold call.")
    await ref_hfp_protocol.execute_command("AT+CHLD=2")

    self.logger.info("[DUT] Wait for call state to be ACTIVE.")
    await self._wait_for_call_state(dut_telecom_cb, _CallState.ACTIVE)

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call state to be NO_CALLS_HELD.",
    ):
      call_setup_state = await ag_indicators[_AgIndicator.CALL_HELD].get()
      self.assertEqual(
          call_setup_state, hfp.CallHeldAgIndicator.NO_CALLS_HELD
      )

Tests HFP connection establishment right after a pairing session.

Test steps
  1. Setup HFP on REF.
  2. Create bond from DUT.
  3. Wait HFP connected on DUT.(Android should autoconnect HFP as AG)
Source code in navi/tests/smoke/hfp_ag_test.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
async def test_pair_and_connect(self) -> None:
  """Tests HFP connection establishment right after a pairing session.

  Test steps:
    1. Setup HFP on REF.
    2. Create bond from DUT.
    3. Wait HFP connected on DUT.(Android should autoconnect HFP as AG)
  """
  with (self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb,):
    hfp_ext.HfProtocol.setup_server(
        self.ref.device,
        sdp_handle=_HFP_SDP_HANDLE,
        configuration=self._default_hfp_configuration(),
    )

    self.logger.info("[DUT] Connect and pair REF.")
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Tests HFP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from REF.
  4. Wait HFP connected on DUT.
  5. Disconnect from REF.
  6. Wait HFP disconnected on DUT.
Source code in navi/tests/smoke/hfp_ag_test.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
async def test_paired_connect_incoming(self) -> None:
  """Tests HFP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from REF.
    4. Wait HFP connected on DUT.
    5. Disconnect from REF.
    6. Wait HFP disconnected on DUT.
  """
  dut_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
  self.test_case_context.push(dut_cb)
  await self.test_pair_and_connect()

  await self._terminate_connection_from_dut()

  self.logger.info("[REF] Reconnect.")
  dut_ref_acl = await self.ref.device.connect(
      self.dut.address,
      core.BT_BR_EDR_TRANSPORT,
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

  self.logger.info("[REF] Authenticate and encrypt connection.")
  await dut_ref_acl.authenticate()
  await dut_ref_acl.encrypt()

  rfcomm_channel = await rfcomm.find_rfcomm_channel_with_uuid(
      dut_ref_acl, core.BT_HANDSFREE_AUDIO_GATEWAY_SERVICE
  )
  if rfcomm_channel is None:
    self.fail("No HFP RFCOMM channel found on REF.")
  self.logger.info("[REF] Found HFP RFCOMM channel %s.", rfcomm_channel)

  self.logger.info("[REF] Open RFCOMM Multiplexer.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    multiplexer = await rfcomm.Client(dut_ref_acl).start()

  self.logger.info("[REF] Open RFCOMM DLC.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    dlc = await multiplexer.open_dlc(rfcomm_channel)

  self.logger.info("[REF] Establish SLC.")
  ref_hfp_protocol = hfp_ext.HfProtocol(
      dlc, self._default_hfp_configuration()
  )
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await ref_hfp_protocol.initiate_slc()

  self.logger.info("[DUT] Wait for HFP connected.")
  await dut_cb.wait_for_event(
      bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

  self.logger.info("[REF] Disconnect.")
  await dut_ref_acl.disconnect()

  self.logger.info("[DUT] Wait for HFP disconnected.")
  await dut_cb.wait_for_event(
      bl4a_api.ProfileActiveDeviceChanged(address=None),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

Tests HFP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from DUT.
  4. Wait HFP connected on DUT.
  5. Disconnect from DUT.
  6. Wait HFP disconnected on DUT.
Source code in navi/tests/smoke/hfp_ag_test.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
async def test_paired_connect_outgoing(self) -> None:
  """Tests HFP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from DUT.
    4. Wait HFP connected on DUT.
    5. Disconnect from DUT.
    6. Wait HFP disconnected on DUT.
  """
  with (self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb,):
    await self.test_pair_and_connect()
    ref_address = self.ref.address

    await self._terminate_connection_from_dut()

    self.logger.info("[DUT] Reconnect.")
    self.dut.bt.connect(ref_address)

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[DUT] Disconnect.")
    self.dut.bt.disconnect(ref_address)

    self.logger.info("[DUT] Wait for HFP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=None),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Tests querying call status from HF.

Test steps
  1. Setup HFP connection.
  2. Place a call.
  3. Query call status from HF.
  4. Terminate the call.
  5. Query call status from HF.
Source code in navi/tests/smoke/hfp_ag_test.py
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
async def test_query_call_status(self) -> None:
  """Tests querying call status from HF.

  Test steps:
    1. Setup HFP connection.
    2. Place a call.
    3. Query call status from HF.
    4. Terminate the call.
    5. Query call status from HF.
  """

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[],
      supported_hf_indicators=[],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  self.logger.info("[DUT] Connect and pair REF.")
  with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
    await self.classic_connect_and_pair()
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

  ag_indicators = collections.defaultdict[
      hfp.AgIndicator, asyncio.Queue[int]
  ](asyncio.Queue)

  def on_ag_indicator(ag_indicator: hfp.AgIndicatorState) -> None:
    ag_indicators[ag_indicator.indicator].put_nowait(
        ag_indicator.current_status
    )

  ref_hfp_protocol.on(ref_hfp_protocol.EVENT_AG_INDICATOR, on_ag_indicator)

  self.logger.info("[DUT] Make incoming call.")
  with self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.INCOMING,
  ):
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      call_setup_state = await ag_indicators[_AgIndicator.CALL_SETUP].get()
      self.assertEqual(call_setup_state, 1)

    calls = await ref_hfp_protocol.query_current_calls()
    self.assertLen(calls, 1)
    self.assertEqual(
        calls[0].direction,
        hfp.CallInfoDirection.MOBILE_TERMINATED_CALL,
    )
    self.assertEqual(calls[0].status, hfp.CallInfoStatus.INCOMING)
    self.assertEqual(calls[0].number, _CALLER_NUMBER)

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    call_setup_state = await ag_indicators[_AgIndicator.CALL_SETUP].get()
    self.assertEqual(call_setup_state, 0)

  calls = await ref_hfp_protocol.query_current_calls()
  self.assertEmpty(calls)

Tests updating battery level indicator from HF.

Test steps
  1. Setup HFP connection.
  2. Send battery level indicator from HF.
  3. Verify call ag indicator.
Source code in navi/tests/smoke/hfp_ag_test.py
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
async def test_update_battery_level(self) -> None:
  """Tests updating battery level indicator from HF.

  Test steps:
    1. Setup HFP connection.
    2. Send battery level indicator from HF.
    3. Verify call ag indicator.
  """

  # [REF] Setup HFP.
  hfp_configuration = hfp.HfConfiguration(
      supported_hf_features=[hfp.HfFeature.HF_INDICATORS],
      supported_hf_indicators=[hfp.HfIndicator.BATTERY_LEVEL],
      supported_audio_codecs=[hfp.AudioCodec.CVSD],
  )
  ref_hfp_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.ref.device,
      sdp_handle=_HFP_SDP_HANDLE,
      configuration=hfp_configuration,
  )

  with (
      self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb,
      self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_adapter_cb,
  ):
    await self.classic_connect_and_pair()
    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for HFP connected.",
    ):
      ref_hfp_protocol = await ref_hfp_protocol_queue.get()

    if not ref_hfp_protocol.supports_ag_feature(hfp.AgFeature.HF_INDICATORS):
      raise signals.TestSkip("DUT doesn't support HF Indicator")

    for i in range(101):
      await ref_hfp_protocol.execute_command(
          f"AT+BIEV={hfp.HfIndicator.BATTERY_LEVEL.value},{i}"
      )
      event = await dut_adapter_cb.wait_for_event(
          bl4a_api.BatteryLevelChanged,
          predicate=lambda e: (e.address == self.ref.address),
      )
      self.assertEqual(event.level, i)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/hfp_hf_test.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
class HfpHfTest(navi_test_base.TwoDevicesTestBase):
  ag_protocol: hfp.AgProtocol | None = None
  ref_hfp_protocols: asyncio.Queue[hfp.AgProtocol]

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.device.is_emulator:
      self.dut.setprop(android_constants.Property.HFP_HF_ENABLED, "true")
      self.dut.setprop(_PROPERTY_HF_FEATURES, "0x1b5")

    if self.dut.getprop(android_constants.Property.HFP_HF_ENABLED) != "true":
      raise signals.TestAbortClass("DUT does not have HFP HF enabled.")

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self.ref_hfp_protocols = asyncio.Queue[hfp.AgProtocol]()

  @classmethod
  def _ag_configuration(
      cls,
      supported_ag_features: Iterable[hfp.AgFeature] = (),
      supported_ag_indicators: Sequence[hfp.AgIndicatorState] = (),
      supported_hf_indicators: Iterable[hfp.HfIndicator] = (),
      supported_ag_call_hold_operations: Iterable[hfp.CallHoldOperation] = (),
      supported_audio_codecs: Iterable[hfp.AudioCodec] = (),
  ) -> hfp.AgConfiguration:
    return hfp.AgConfiguration(
        supported_ag_features=(
            supported_ag_features
            or [
                hfp.AgFeature.ENHANCED_CALL_STATUS,
            ]
        ),
        supported_ag_indicators=(
            supported_ag_indicators
            or [
                hfp.AgIndicatorState.call(),
                hfp.AgIndicatorState.callsetup(),
                hfp.AgIndicatorState.service(),
                hfp.AgIndicatorState.signal(),
                hfp.AgIndicatorState.roam(),
                hfp.AgIndicatorState.callheld(),
                hfp.AgIndicatorState.battchg(),
            ]
        ),
        supported_hf_indicators=supported_hf_indicators or [],
        supported_ag_call_hold_operations=(
            supported_ag_call_hold_operations or []
        ),
        supported_audio_codecs=supported_audio_codecs or [hfp.AudioCodec.CVSD],
    )

  async def _terminate_connection_from_ref(self) -> None:
    if not (
        dut_ref_acl := self.ref.device.find_connection_by_bd_addr(
            hci.Address(self.dut.address)
        )
    ):
      return

    self.logger.info("[REF] Terminate connection.")
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      await dut_ref_acl.disconnect()
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  async def _wait_for_hfp_state(
      self, dut_cb: _Callback, state: _HfpState
  ) -> None:
    self.logger.info("[DUT] Wait for HFP state %s.", state)
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=state,
        ),
    )

  def _setup_ag_device(self, configuration: hfp.AgConfiguration) -> None:
    def on_dlc(dlc: rfcomm.DLC):
      self.ref_hfp_protocols.put_nowait(hfp.AgProtocol(dlc, configuration))

    self.ref.device.sdp_service_records = {
        _HFP_AG_SDP_HANDLE: hfp.make_ag_sdp_records(
            service_record_handle=_HFP_AG_SDP_HANDLE,
            rfcomm_channel=rfcomm.Server(self.ref.device).listen(on_dlc),
            configuration=configuration,
        )
    }

  async def _connect_hfp_from_ref(
      self, config: hfp.AgConfiguration
  ) -> hfp.AgProtocol:
    if not (
        dut_ref_acl := self.ref.device.find_connection_by_bd_addr(
            hci.Address(self.dut.address)
        )
    ):
      self.logger.info("[REF] Connect.")
      dut_ref_acl = await self.ref.device.connect(
          self.dut.address,
          core.BT_BR_EDR_TRANSPORT,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[REF] Authenticate and encrypt connection.")
      await dut_ref_acl.authenticate()
      await dut_ref_acl.encrypt()

    sdp_record = await hfp.find_hf_sdp_record(dut_ref_acl)
    if not sdp_record:
      self.fail("DUT does not have HFP SDP record.")
    rfcomm_channel = sdp_record[0]

    self.logger.info("[REF] Found HFP RFCOMM channel %s.", rfcomm_channel)

    self.logger.info("[REF] Open RFCOMM Channel.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      multiplexer = await rfcomm.Client(dut_ref_acl).start()
      dlc = await multiplexer.open_dlc(rfcomm_channel)
    return hfp.AgProtocol(dlc, config)

  async def test_pair_and_connect(self) -> None:
    """Tests HFP connection establishment right after a pairing session.

    Test steps:
      1. Setup HFP on REF.
      2. Create bond from DUT.
      3. Wait HFP connected on DUT.(Android should autoconnect HFP as HF)
    """
    config = self._ag_configuration()
    self._setup_ag_device(config)

    self.logger.info("[DUT] Connect and pair REF.")
    with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for HFP connected.")
      await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

  async def test_paired_connect_outgoing(self) -> None:
    """Tests HFP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from DUT.
      4. Wait HFP connected on DUT.
      5. Disconnect from DUT.
      6. Wait HFP disconnected on DUT.
    """
    await self.test_pair_and_connect()
    await self._terminate_connection_from_ref()

    with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:

      self.logger.info("[DUT] Reconnect.")
      self.dut.bt.connect(self.ref.address)
      await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

      self.logger.info("[DUT] Disconnect.")
      self.dut.bt.disconnect(self.ref.address)
      await self._wait_for_hfp_state(dut_cb, _HfpState.DISCONNECTED)

  async def test_paired_connect_incoming(self) -> None:
    """Tests HFP connection establishment where pairing is not involved.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Trigger connection from REF.
      4. Wait HFP connected on DUT.
      5. Disconnect from REF.
      6. Wait HFP disconnected on DUT.
    """
    configuration = self._ag_configuration()
    await self.test_pair_and_connect()
    await self._terminate_connection_from_ref()

    with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
      await self._connect_hfp_from_ref(configuration)

      self.logger.info("[DUT] Wait for HFP connected.")
      await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

      await self._terminate_connection_from_ref()
      await self._wait_for_hfp_state(dut_cb, _HfpState.DISCONNECTED)

  @navi_test_base.parameterized(
      hfp.AudioCodec.CVSD,
      hfp.AudioCodec.MSBC,
      hfp.AudioCodec.LC3_SWB,
  )
  async def test_sco_connection_with_codec_negotiation(
      self, codec: hfp.AudioCodec
  ) -> None:
    """Tests SCO connection establishment.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Make SCO connection from AG(REF).
      3. Terminate SCO connection from AG(REF).

    Args:
      codec: Codec used in the SCO connection.
    """

    self._setup_ag_device(
        self._ag_configuration(
            supported_audio_codecs=[
                hfp.AudioCodec.CVSD,
                hfp.AudioCodec.MSBC,
                hfp.AudioCodec.LC3_SWB,
            ],
            supported_ag_features=[
                hfp.AgFeature.ENHANCED_CALL_STATUS,
                hfp.AgFeature.CODEC_NEGOTIATION,
            ],
        )
    )

    match codec:
      case hfp.AudioCodec.CVSD:
        esco_parameters = hfp.ESCO_PARAMETERS[
            hfp.DefaultCodecParameters.ESCO_CVSD_S4
        ]
      case hfp.AudioCodec.MSBC:
        esco_parameters = hfp.ESCO_PARAMETERS[
            hfp.DefaultCodecParameters.ESCO_MSBC_T2
        ]
      case hfp.AudioCodec.LC3_SWB:
        if self.dut.getprop(_PROPERTY_SWB_SUPPORTED) != "true":
          raise signals.TestSkip("SWB is not supported on the device.")
        esco_parameters = hfp_ext.ESCO_PARAMETERS_LC3_T2
      case _:
        self.fail(f"Unsupported codec: {codec}")

    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_hfp_cb,
    ):
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for HFP connected.")
      await self._wait_for_hfp_state(dut_hfp_cb, _HfpState.CONNECTED)

      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        ref_hfp_protocol = await self.ref_hfp_protocols.get()

      # WearService may disable the audio route.
      self.dut.bt.hfpHfSetAudioRouteAllowed(self.ref.address, True)

      self.logger.info("[REF] Negotiate codec.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref_hfp_protocol.negotiate_codec(codec)

      self.logger.info("[REF] Create SCO.")
      connection = ref_hfp_protocol.dlc.multiplexer.l2cap_channel.connection
      sco_links = asyncio.Queue[bumble_device.ScoLink]()
      self.ref.device.on(
          self.ref.device.EVENT_SCO_CONNECTION, sco_links.put_nowait
      )
      await self.ref.device.send_command(
          hci.HCI_Enhanced_Setup_Synchronous_Connection_Command(
              connection_handle=connection.handle, **esco_parameters.asdict()
          )
      )

      self.logger.info("[DUT] Wait for SCO connected.")
      await dut_hfp_cb.wait_for_event(
          bl4a_api.HfpHfAudioStateChanged(
              address=self.ref.address, state=_HfpState.CONNECTED
          ),
      )

      self.logger.info("[REF] Terminate SCO.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        sco_link = await sco_links.get()
        await sco_link.disconnect()

      self.logger.info("[DUT] Wait for SCO disconnected.")
      await dut_hfp_cb.wait_for_event(
          bl4a_api.HfpHfAudioStateChanged(
              address=self.ref.address, state=_HfpState.DISCONNECTED
          ),
      )

  @navi_test_base.parameterized(
      constants.TestRole.DUT,
      constants.TestRole.REF,
  )
  async def test_set_volume(self, issuer: constants.TestRole) -> None:
    """Tests setting speaker volume over HFP.

    Test steps:
      1. Setup HFP connection between DUT and REF.
      2. Set volume from DUT or REF.
      3. Check the volume on the other side.

    Args:
      issuer: Device which requests the volume.
    """
    configuration = self._ag_configuration()
    self._setup_ag_device(configuration)

    max_system_call_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_CALL)
    min_system_call_volume = self.dut.bt.getMinVolume(_STREAM_TYPE_CALL)

    def system_call_volume_to_hfp_volume(system_call_volume: int) -> int:
      return (
          (system_call_volume - min_system_call_volume)
          * (_MAX_HFP_VOLUME - _MIN_HFP_VOLUME)
          // (max_system_call_volume - min_system_call_volume)
      ) + _MIN_HFP_VOLUME

    # TODO: Remove volume reset before test when conversion is
    # fixed.
    self.dut.bt.setVolume(_STREAM_TYPE_CALL, 2)
    expect_dut_volume = max(
        min_system_call_volume,
        (self.dut.bt.getVolume(_STREAM_TYPE_CALL) + 1)
        % (max_system_call_volume + 1),
    )
    expect_hfp_volume = system_call_volume_to_hfp_volume(expect_dut_volume)

    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_hfp_cb,
        self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb,
    ):
      await self.classic_connect_and_pair()

      ref_volumes = asyncio.Queue[int]()
      self.logger.info("[REF] Wait for HFP connected.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        ref_hfp_protocol = await self.ref_hfp_protocols.get()
      ref_hfp_protocol.on(
          ref_hfp_protocol.EVENT_SPEAKER_VOLUME, ref_volumes.put_nowait
      )

      self.logger.info("[DUT] Wait for HFP connected.")
      await self._wait_for_hfp_state(dut_hfp_cb, _HfpState.CONNECTED)

      self.logger.info("[REF] Wait for initial volume.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref_volumes.get()

      if issuer == constants.TestRole.DUT:
        self.logger.info("[DUT] Set volume to %d.", expect_dut_volume)
        self.dut.bt.setVolume(_STREAM_TYPE_CALL, expect_dut_volume)

        self.logger.info("[REF] Wait for volume changed.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          actual_hfp_volume = await ref_volumes.get()
          self.assertEqual(actual_hfp_volume, expect_hfp_volume)
      else:
        self.logger.info("[REF] Set volume to %d.", expect_hfp_volume)
        ref_hfp_protocol.set_speaker_volume(expect_hfp_volume)

      self.logger.info("[DUT] Wait for volume changed.")
      await dut_audio_cb.wait_for_event(
          bl4a_api.VolumeChanged(
              stream_type=_STREAM_TYPE_CALL, volume_value=expect_dut_volume
          )
      )

  async def test_update_battery_level(self) -> None:
    """Tests updating battery level indicator from HF.

    Test steps:
      1. Setup HFP connection between DUT and REF.
      2. Update battery indicator from HF.
      3. Check the battery indicator from AG.
    """
    configuration = self._ag_configuration(
        supported_ag_features=[
            hfp.AgFeature.HF_INDICATORS,
            hfp.AgFeature.ENHANCED_CALL_STATUS,
            # TODO: Remove this feature when feature check is
            # fixed on Android.
            hfp.AgFeature.EXTENDED_ERROR_RESULT_CODES,
        ],
        supported_hf_indicators=[
            hfp.HfIndicator.BATTERY_LEVEL,
        ],
    )
    self._setup_ag_device(configuration)
    # Mock battery level to avoid unexpected changing during the test.
    initial_battery_level = 50
    self.dut.shell(f"dumpsys battery set level {initial_battery_level}")

    with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
      await self.classic_connect_and_pair()

      self.logger.info("[REF] Wait for HFP connected.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        ref_hfp_protocol = await self.ref_hfp_protocols.get()

      hf_indicators = asyncio.Queue[hfp.HfIndicatorState]()
      ref_hfp_protocol.on(
          ref_hfp_protocol.EVENT_HF_INDICATOR, hf_indicators.put_nowait
      )

      self.logger.info("[DUT] Wait for HFP connected.")
      await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

      self.logger.info("[REF] Wait for initial battery level.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        hf_indicator = await hf_indicators.get()
        # The battery initial battery level cannot be mocked, so we don't check
        # the value here.
        self.assertEqual(hf_indicator.indicator, hfp.HfIndicator.BATTERY_LEVEL)

      # Set battery level.
      expected_battery_level = 100
      self.dut.shell(f"dumpsys battery set level {expected_battery_level}")

      self.logger.info("[REF] Wait for updated battery level.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        hf_indicator = await hf_indicators.get()
        self.assertEqual(hf_indicator.indicator, hfp.HfIndicator.BATTERY_LEVEL)
        self.assertEqual(hf_indicator.current_status, expected_battery_level)

  @navi_test_base.named_parameterized(
      accept=True,
      reject=False,
  )
  async def test_answer_call_from_hf(self, accepted: bool) -> None:
    """Tests answering call from HF.

    Test steps:
      1. Setup HFP connection between DUT and REF.
      2. Answer call from HF.
      3. Check the call state from AG.

    Args:
      accepted: Whether the call is accepted or rejected.
    """
    configuration = self._ag_configuration(
        supported_ag_features=[
            hfp.AgFeature.ENHANCED_CALL_STATUS,
            hfp.AgFeature.CODEC_NEGOTIATION,
        ],
        supported_ag_indicators=[
            hfp.AgIndicatorState.call(),
            hfp.AgIndicatorState.callsetup(),
        ],
    )
    self._setup_ag_device(configuration)

    hf_cb = self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF)
    telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
    self.test_case_context.push(hf_cb)
    self.test_case_context.push(telecom_cb)

    await self.classic_connect_and_pair()

    self.logger.info("[REF] Wait for HFP connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hfp_protocol = await self.ref_hfp_protocols.get()

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(hf_cb, _HfpState.CONNECTED)

    self.logger.info("[REF] Update call state.")
    call_info = hfp.CallInfo(
        index=1,
        direction=hfp.CallInfoDirection.MOBILE_TERMINATED_CALL,
        status=hfp.CallInfoStatus.INCOMING,
        mode=hfp.CallInfoMode.VOICE,
        multi_party=hfp.CallInfoMultiParty.NOT_IN_CONFERENCE,
        number="+1234567890",
    )
    ref_hfp_protocol.calls.append(call_info)
    ref_hfp_protocol.update_ag_indicator(
        hfp.AgIndicator.CALL_SETUP,
        hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
    )

    self.logger.info("[DUT] Wait for call ringing.")
    await telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=_CallState.RINGING,
        )
    )
    if accepted:
      answered = asyncio.Event()
      ref_hfp_protocol.once(ref_hfp_protocol.EVENT_ANSWER, answered.set)
      self.logger.info("[DUT] Answer call.")
      self.dut.shell("input keyevent KEYCODE_CALL")
      self.logger.info("[REF] Wait for call answered.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await answered.wait()
    else:
      rejected = asyncio.Event()
      ref_hfp_protocol.once(ref_hfp_protocol.EVENT_HANG_UP, rejected.set)
      self.logger.info("[DUT] Reject call.")
      self.dut.shell("input keyevent KEYCODE_ENDCALL")
      self.logger.info("[REF] Wait for call rejected.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await rejected.wait()

    self.logger.info("[REF] Update call state.")
    ref_hfp_protocol.update_ag_indicator(
        hfp.AgIndicator.CALL_SETUP,
        hfp.CallSetupAgIndicator.NOT_IN_CALL_SETUP,
    )
    if accepted:
      call_info.status = hfp.CallInfoStatus.ACTIVE
      ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 1)
    else:
      ref_hfp_protocol.calls.clear()

    self.logger.info("[DUT] Wait for call state changed.")
    await telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=(_CallState.ACTIVE if accepted else _CallState.DISCONNECTED),
        )
    )

Tests answering call from HF.

Test steps
  1. Setup HFP connection between DUT and REF.
  2. Answer call from HF.
  3. Check the call state from AG.

Parameters:

Name Type Description Default
accepted bool

Whether the call is accepted or rejected.

required
Source code in navi/tests/smoke/hfp_hf_test.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
@navi_test_base.named_parameterized(
    accept=True,
    reject=False,
)
async def test_answer_call_from_hf(self, accepted: bool) -> None:
  """Tests answering call from HF.

  Test steps:
    1. Setup HFP connection between DUT and REF.
    2. Answer call from HF.
    3. Check the call state from AG.

  Args:
    accepted: Whether the call is accepted or rejected.
  """
  configuration = self._ag_configuration(
      supported_ag_features=[
          hfp.AgFeature.ENHANCED_CALL_STATUS,
          hfp.AgFeature.CODEC_NEGOTIATION,
      ],
      supported_ag_indicators=[
          hfp.AgIndicatorState.call(),
          hfp.AgIndicatorState.callsetup(),
      ],
  )
  self._setup_ag_device(configuration)

  hf_cb = self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF)
  telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
  self.test_case_context.push(hf_cb)
  self.test_case_context.push(telecom_cb)

  await self.classic_connect_and_pair()

  self.logger.info("[REF] Wait for HFP connected.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_hfp_protocol = await self.ref_hfp_protocols.get()

  self.logger.info("[DUT] Wait for HFP connected.")
  await self._wait_for_hfp_state(hf_cb, _HfpState.CONNECTED)

  self.logger.info("[REF] Update call state.")
  call_info = hfp.CallInfo(
      index=1,
      direction=hfp.CallInfoDirection.MOBILE_TERMINATED_CALL,
      status=hfp.CallInfoStatus.INCOMING,
      mode=hfp.CallInfoMode.VOICE,
      multi_party=hfp.CallInfoMultiParty.NOT_IN_CONFERENCE,
      number="+1234567890",
  )
  ref_hfp_protocol.calls.append(call_info)
  ref_hfp_protocol.update_ag_indicator(
      hfp.AgIndicator.CALL_SETUP,
      hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
  )

  self.logger.info("[DUT] Wait for call ringing.")
  await telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=_CallState.RINGING,
      )
  )
  if accepted:
    answered = asyncio.Event()
    ref_hfp_protocol.once(ref_hfp_protocol.EVENT_ANSWER, answered.set)
    self.logger.info("[DUT] Answer call.")
    self.dut.shell("input keyevent KEYCODE_CALL")
    self.logger.info("[REF] Wait for call answered.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await answered.wait()
  else:
    rejected = asyncio.Event()
    ref_hfp_protocol.once(ref_hfp_protocol.EVENT_HANG_UP, rejected.set)
    self.logger.info("[DUT] Reject call.")
    self.dut.shell("input keyevent KEYCODE_ENDCALL")
    self.logger.info("[REF] Wait for call rejected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await rejected.wait()

  self.logger.info("[REF] Update call state.")
  ref_hfp_protocol.update_ag_indicator(
      hfp.AgIndicator.CALL_SETUP,
      hfp.CallSetupAgIndicator.NOT_IN_CALL_SETUP,
  )
  if accepted:
    call_info.status = hfp.CallInfoStatus.ACTIVE
    ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 1)
  else:
    ref_hfp_protocol.calls.clear()

  self.logger.info("[DUT] Wait for call state changed.")
  await telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=(_CallState.ACTIVE if accepted else _CallState.DISCONNECTED),
      )
  )

Tests HFP connection establishment right after a pairing session.

Test steps
  1. Setup HFP on REF.
  2. Create bond from DUT.
  3. Wait HFP connected on DUT.(Android should autoconnect HFP as HF)
Source code in navi/tests/smoke/hfp_hf_test.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
async def test_pair_and_connect(self) -> None:
  """Tests HFP connection establishment right after a pairing session.

  Test steps:
    1. Setup HFP on REF.
    2. Create bond from DUT.
    3. Wait HFP connected on DUT.(Android should autoconnect HFP as HF)
  """
  config = self._ag_configuration()
  self._setup_ag_device(config)

  self.logger.info("[DUT] Connect and pair REF.")
  with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

Tests HFP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from REF.
  4. Wait HFP connected on DUT.
  5. Disconnect from REF.
  6. Wait HFP disconnected on DUT.
Source code in navi/tests/smoke/hfp_hf_test.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
async def test_paired_connect_incoming(self) -> None:
  """Tests HFP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from REF.
    4. Wait HFP connected on DUT.
    5. Disconnect from REF.
    6. Wait HFP disconnected on DUT.
  """
  configuration = self._ag_configuration()
  await self.test_pair_and_connect()
  await self._terminate_connection_from_ref()

  with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
    await self._connect_hfp_from_ref(configuration)

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

    await self._terminate_connection_from_ref()
    await self._wait_for_hfp_state(dut_cb, _HfpState.DISCONNECTED)

Tests HFP connection establishment where pairing is not involved.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Trigger connection from DUT.
  4. Wait HFP connected on DUT.
  5. Disconnect from DUT.
  6. Wait HFP disconnected on DUT.
Source code in navi/tests/smoke/hfp_hf_test.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
async def test_paired_connect_outgoing(self) -> None:
  """Tests HFP connection establishment where pairing is not involved.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Trigger connection from DUT.
    4. Wait HFP connected on DUT.
    5. Disconnect from DUT.
    6. Wait HFP disconnected on DUT.
  """
  await self.test_pair_and_connect()
  await self._terminate_connection_from_ref()

  with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:

    self.logger.info("[DUT] Reconnect.")
    self.dut.bt.connect(self.ref.address)
    await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

    self.logger.info("[DUT] Disconnect.")
    self.dut.bt.disconnect(self.ref.address)
    await self._wait_for_hfp_state(dut_cb, _HfpState.DISCONNECTED)

Tests SCO connection establishment.

Test steps
  1. Setup pairing between DUT and REF.
  2. Make SCO connection from AG(REF).
  3. Terminate SCO connection from AG(REF).

Parameters:

Name Type Description Default
codec AudioCodec

Codec used in the SCO connection.

required
Source code in navi/tests/smoke/hfp_hf_test.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
@navi_test_base.parameterized(
    hfp.AudioCodec.CVSD,
    hfp.AudioCodec.MSBC,
    hfp.AudioCodec.LC3_SWB,
)
async def test_sco_connection_with_codec_negotiation(
    self, codec: hfp.AudioCodec
) -> None:
  """Tests SCO connection establishment.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Make SCO connection from AG(REF).
    3. Terminate SCO connection from AG(REF).

  Args:
    codec: Codec used in the SCO connection.
  """

  self._setup_ag_device(
      self._ag_configuration(
          supported_audio_codecs=[
              hfp.AudioCodec.CVSD,
              hfp.AudioCodec.MSBC,
              hfp.AudioCodec.LC3_SWB,
          ],
          supported_ag_features=[
              hfp.AgFeature.ENHANCED_CALL_STATUS,
              hfp.AgFeature.CODEC_NEGOTIATION,
          ],
      )
  )

  match codec:
    case hfp.AudioCodec.CVSD:
      esco_parameters = hfp.ESCO_PARAMETERS[
          hfp.DefaultCodecParameters.ESCO_CVSD_S4
      ]
    case hfp.AudioCodec.MSBC:
      esco_parameters = hfp.ESCO_PARAMETERS[
          hfp.DefaultCodecParameters.ESCO_MSBC_T2
      ]
    case hfp.AudioCodec.LC3_SWB:
      if self.dut.getprop(_PROPERTY_SWB_SUPPORTED) != "true":
        raise signals.TestSkip("SWB is not supported on the device.")
      esco_parameters = hfp_ext.ESCO_PARAMETERS_LC3_T2
    case _:
      self.fail(f"Unsupported codec: {codec}")

  with (
      self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_hfp_cb,
  ):
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(dut_hfp_cb, _HfpState.CONNECTED)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hfp_protocol = await self.ref_hfp_protocols.get()

    # WearService may disable the audio route.
    self.dut.bt.hfpHfSetAudioRouteAllowed(self.ref.address, True)

    self.logger.info("[REF] Negotiate codec.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_hfp_protocol.negotiate_codec(codec)

    self.logger.info("[REF] Create SCO.")
    connection = ref_hfp_protocol.dlc.multiplexer.l2cap_channel.connection
    sco_links = asyncio.Queue[bumble_device.ScoLink]()
    self.ref.device.on(
        self.ref.device.EVENT_SCO_CONNECTION, sco_links.put_nowait
    )
    await self.ref.device.send_command(
        hci.HCI_Enhanced_Setup_Synchronous_Connection_Command(
            connection_handle=connection.handle, **esco_parameters.asdict()
        )
    )

    self.logger.info("[DUT] Wait for SCO connected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.HfpHfAudioStateChanged(
            address=self.ref.address, state=_HfpState.CONNECTED
        ),
    )

    self.logger.info("[REF] Terminate SCO.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      sco_link = await sco_links.get()
      await sco_link.disconnect()

    self.logger.info("[DUT] Wait for SCO disconnected.")
    await dut_hfp_cb.wait_for_event(
        bl4a_api.HfpHfAudioStateChanged(
            address=self.ref.address, state=_HfpState.DISCONNECTED
        ),
    )

Tests setting speaker volume over HFP.

Test steps
  1. Setup HFP connection between DUT and REF.
  2. Set volume from DUT or REF.
  3. Check the volume on the other side.

Parameters:

Name Type Description Default
issuer TestRole

Device which requests the volume.

required
Source code in navi/tests/smoke/hfp_hf_test.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
@navi_test_base.parameterized(
    constants.TestRole.DUT,
    constants.TestRole.REF,
)
async def test_set_volume(self, issuer: constants.TestRole) -> None:
  """Tests setting speaker volume over HFP.

  Test steps:
    1. Setup HFP connection between DUT and REF.
    2. Set volume from DUT or REF.
    3. Check the volume on the other side.

  Args:
    issuer: Device which requests the volume.
  """
  configuration = self._ag_configuration()
  self._setup_ag_device(configuration)

  max_system_call_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_CALL)
  min_system_call_volume = self.dut.bt.getMinVolume(_STREAM_TYPE_CALL)

  def system_call_volume_to_hfp_volume(system_call_volume: int) -> int:
    return (
        (system_call_volume - min_system_call_volume)
        * (_MAX_HFP_VOLUME - _MIN_HFP_VOLUME)
        // (max_system_call_volume - min_system_call_volume)
    ) + _MIN_HFP_VOLUME

  # TODO: Remove volume reset before test when conversion is
  # fixed.
  self.dut.bt.setVolume(_STREAM_TYPE_CALL, 2)
  expect_dut_volume = max(
      min_system_call_volume,
      (self.dut.bt.getVolume(_STREAM_TYPE_CALL) + 1)
      % (max_system_call_volume + 1),
  )
  expect_hfp_volume = system_call_volume_to_hfp_volume(expect_dut_volume)

  with (
      self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_hfp_cb,
      self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb,
  ):
    await self.classic_connect_and_pair()

    ref_volumes = asyncio.Queue[int]()
    self.logger.info("[REF] Wait for HFP connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hfp_protocol = await self.ref_hfp_protocols.get()
    ref_hfp_protocol.on(
        ref_hfp_protocol.EVENT_SPEAKER_VOLUME, ref_volumes.put_nowait
    )

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(dut_hfp_cb, _HfpState.CONNECTED)

    self.logger.info("[REF] Wait for initial volume.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_volumes.get()

    if issuer == constants.TestRole.DUT:
      self.logger.info("[DUT] Set volume to %d.", expect_dut_volume)
      self.dut.bt.setVolume(_STREAM_TYPE_CALL, expect_dut_volume)

      self.logger.info("[REF] Wait for volume changed.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        actual_hfp_volume = await ref_volumes.get()
        self.assertEqual(actual_hfp_volume, expect_hfp_volume)
    else:
      self.logger.info("[REF] Set volume to %d.", expect_hfp_volume)
      ref_hfp_protocol.set_speaker_volume(expect_hfp_volume)

    self.logger.info("[DUT] Wait for volume changed.")
    await dut_audio_cb.wait_for_event(
        bl4a_api.VolumeChanged(
            stream_type=_STREAM_TYPE_CALL, volume_value=expect_dut_volume
        )
    )

Tests updating battery level indicator from HF.

Test steps
  1. Setup HFP connection between DUT and REF.
  2. Update battery indicator from HF.
  3. Check the battery indicator from AG.
Source code in navi/tests/smoke/hfp_hf_test.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
async def test_update_battery_level(self) -> None:
  """Tests updating battery level indicator from HF.

  Test steps:
    1. Setup HFP connection between DUT and REF.
    2. Update battery indicator from HF.
    3. Check the battery indicator from AG.
  """
  configuration = self._ag_configuration(
      supported_ag_features=[
          hfp.AgFeature.HF_INDICATORS,
          hfp.AgFeature.ENHANCED_CALL_STATUS,
          # TODO: Remove this feature when feature check is
          # fixed on Android.
          hfp.AgFeature.EXTENDED_ERROR_RESULT_CODES,
      ],
      supported_hf_indicators=[
          hfp.HfIndicator.BATTERY_LEVEL,
      ],
  )
  self._setup_ag_device(configuration)
  # Mock battery level to avoid unexpected changing during the test.
  initial_battery_level = 50
  self.dut.shell(f"dumpsys battery set level {initial_battery_level}")

  with self.dut.bl4a.register_callback(bl4a_api.Module.HFP_HF) as dut_cb:
    await self.classic_connect_and_pair()

    self.logger.info("[REF] Wait for HFP connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hfp_protocol = await self.ref_hfp_protocols.get()

    hf_indicators = asyncio.Queue[hfp.HfIndicatorState]()
    ref_hfp_protocol.on(
        ref_hfp_protocol.EVENT_HF_INDICATOR, hf_indicators.put_nowait
    )

    self.logger.info("[DUT] Wait for HFP connected.")
    await self._wait_for_hfp_state(dut_cb, _HfpState.CONNECTED)

    self.logger.info("[REF] Wait for initial battery level.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      hf_indicator = await hf_indicators.get()
      # The battery initial battery level cannot be mocked, so we don't check
      # the value here.
      self.assertEqual(hf_indicator.indicator, hfp.HfIndicator.BATTERY_LEVEL)

    # Set battery level.
    expected_battery_level = 100
    self.dut.shell(f"dumpsys battery set level {expected_battery_level}")

    self.logger.info("[REF] Wait for updated battery level.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      hf_indicator = await hf_indicators.get()
      self.assertEqual(hf_indicator.indicator, hfp.HfIndicator.BATTERY_LEVEL)
      self.assertEqual(hf_indicator.current_status, expected_battery_level)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/hid_test.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
class HidTest(navi_test_base.TwoDevicesTestBase):
  ref_hid_server: hid.Server[hid.DeviceProtocol]
  ref_hid_device: hid.DeviceProtocol

  def _setup_hid_service(self) -> None:
    self.ref_hid_server = hid.Server(self.ref.device, hid.DeviceProtocol)
    self.ref.device.sdp_service_records = {
        1: hid.make_device_sdp_record(1, hogp_test.HID_REPORT_MAP)
    }

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if (
        self.dut.device.adb.getprop(hogp_test.PROPERTY_HID_HOST_SUPPORTED)
        != "true"
    ):
      raise signals.TestAbortClass("HID host is not supported on DUT")

    # Stay awake during the test.
    self.dut.shell("svc power stayon true")
    # Dismiss the keyguard.
    self.dut.shell("wm dismiss-keyguard")

  @override
  async def async_teardown_class(self) -> None:
    await super().async_teardown_class()
    # Stop staying awake during the test.
    self.dut.shell("svc power stayon false")

  async def test_connect(self) -> None:
    """Tests establishing the HID connection from DUT to REF.

    Test steps:
      1. Establish the HID connection between DUT and REF.
      2. Verify the HID connection is established.
    """
    self._setup_hid_service()
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.HID_HOST
    ) as dut_hid_cb:
      self.logger.info("[DUT] Pair with REF")
      await self.classic_connect_and_pair()

      self.logger.info("[DUT] Wait for HID connected")
      await dut_hid_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

      self.logger.info("[REF] Wait for HID connected")
      self.ref_hid_device = await self.ref_hid_server.wait_connection()

  async def test_reconnect(self) -> None:
    """Tests reconnecting the HID connection with the background scanner.

    Test steps:
      1. Pair with REF.
      2. Terminate the connection.
      3. Connect HID from REF.
    """
    await self.test_connect()

    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address)
    )
    assert ref_dut_acl is not None
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.ADAPTER
    ) as dut_adapter_cb:
      self.logger.info("[REF] Disconnect")
      await ref_dut_acl.disconnect()
      await dut_adapter_cb.wait_for_event(bl4a_api.AclDisconnected)

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.HID_HOST
    ) as dut_hid_cb:
      self.logger.info("[REF] Connect ACL")
      ref_dut_acl = await self.ref.device.connect(
          self.dut.address,
          transport=core.BT_BR_EDR_TRANSPORT,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[REF] Encrypt")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref_dut_acl.authenticate()
        await ref_dut_acl.encrypt()

      self.logger.info("[REF] Connect HID")
      self.ref_hid_device = await hid.DeviceProtocol.connect(ref_dut_acl)

      self.logger.info("[DUT] Wait for connected")
      await dut_hid_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

  async def test_keyboard_input(self) -> None:
    """Tests the HID keyboard input.

    Test steps:
      1. Establish the HID connection between DUT and REF.
      2. Press each key on the keyboard and verify the key down and up events
         on DUT.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

    for hid_key in range(
        constants.UsbHidKeyCode.A, constants.UsbHidKeyCode.Z + 1
    ):
      hid_key_code = constants.UsbHidKeyCode(hid_key)
      android_key_code = android_constants.KeyCode[hid_key_code.name]
      self.logger.info("[REF] Press HID key %s", hid_key_code.name)
      self.ref_hid_device.send_data(
          bytes([0x01, 0x00, 0x00, hid_key, 0x00, 0x00, 0x00, 0x00, 0x00])
      )
      self.logger.info("[DUT] Wait for key %s down", android_key_code.name)
      await dut_input_cb.wait_for_event(
          bl4a_api.KeyEvent(
              key_code=android_key_code, action=android_constants.KeyAction.DOWN
          )
      )

      self.logger.info("[REF] Release HID key %s", hid_key_code.name)

      self.logger.info("[DUT] Wait for key %s up", android_key_code.name)
      self.ref_hid_device.send_data(
          bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
      )
      await dut_input_cb.wait_for_event(
          bl4a_api.KeyEvent(
              key_code=android_key_code, action=android_constants.KeyAction.UP
          )
      )

  async def test_mouse_click(self) -> None:
    """Tests the HID mouse click.

    Test steps:
      1. Leverage the test_connect() to establish the connection.
      2. Press primary button and wait for button press.
      3. Release primary button and wait for button down.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

    self.logger.info("[REF] Press Primary button")
    hid_report = struct.pack("<BBhhB", 0x02, 0x01, 0, 0, 0)
    self.ref_hid_device.send_data(hid_report)

    self.logger.info("[DUT] Wait for button press")
    event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
    self.assertEqual(event.action, android_constants.MotionAction.BUTTON_PRESS)

    self.logger.info("[REF] Release Primary button")
    hid_report = struct.pack("<BBhhB", 0x02, 0x00, 0, 0, 0)
    self.ref_hid_device.send_data(hid_report)

    self.logger.info("[DUT] Wait for button down")
    event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
    self.assertEqual(
        event.action, android_constants.MotionAction.BUTTON_RELEASE
    )

  async def test_mouse_movement(self) -> None:
    """Tests the HID mouse movement.

    Test steps:
      1. Leverage the test_connect() to establish the connection.
      2. Move on X axis and wait for hover movement.
      3. Move on Y axis and wait for hover movement.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

    self.logger.info("[REF] Move on X axis")
    hid_report = struct.pack("<BBhhB", 0x02, 0, 1, 0, 0)
    self.ref_hid_device.send_data(hid_report)

    self.logger.info("[DUT] Wait for hover movement")
    await dut_input_cb.wait_for_event(
        bl4a_api.MotionEvent,
        lambda e: e.action
        in (
            android_constants.MotionAction.HOVER_MOVE,
            android_constants.MotionAction.HOVER_ENTER,
            android_constants.MotionAction.HOVER_EXIT,
        ),
    )
    # Clear all events.
    dut_input_cb.get_all_events(bl4a_api.MotionEvent)

    self.logger.info("[REF] Move on Y axis")
    hid_report = struct.pack("<BBhhB", 0x02, 0, 0, 1, 0)
    self.ref_hid_device.send_data(hid_report)

    self.logger.info("[DUT] Wait for hover movement")
    await dut_input_cb.wait_for_event(
        bl4a_api.MotionEvent,
        lambda e: e.action
        in (
            android_constants.MotionAction.HOVER_MOVE,
            android_constants.MotionAction.HOVER_ENTER,
            android_constants.MotionAction.HOVER_EXIT,
        ),
    )

Tests establishing the HID connection from DUT to REF.

Test steps
  1. Establish the HID connection between DUT and REF.
  2. Verify the HID connection is established.
Source code in navi/tests/smoke/hid_test.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
async def test_connect(self) -> None:
  """Tests establishing the HID connection from DUT to REF.

  Test steps:
    1. Establish the HID connection between DUT and REF.
    2. Verify the HID connection is established.
  """
  self._setup_hid_service()
  with self.dut.bl4a.register_callback(
      bl4a_api.Module.HID_HOST
  ) as dut_hid_cb:
    self.logger.info("[DUT] Pair with REF")
    await self.classic_connect_and_pair()

    self.logger.info("[DUT] Wait for HID connected")
    await dut_hid_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF] Wait for HID connected")
    self.ref_hid_device = await self.ref_hid_server.wait_connection()

Tests the HID keyboard input.

Test steps
  1. Establish the HID connection between DUT and REF.
  2. Press each key on the keyboard and verify the key down and up events on DUT.
Source code in navi/tests/smoke/hid_test.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
async def test_keyboard_input(self) -> None:
  """Tests the HID keyboard input.

  Test steps:
    1. Establish the HID connection between DUT and REF.
    2. Press each key on the keyboard and verify the key down and up events
       on DUT.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

  for hid_key in range(
      constants.UsbHidKeyCode.A, constants.UsbHidKeyCode.Z + 1
  ):
    hid_key_code = constants.UsbHidKeyCode(hid_key)
    android_key_code = android_constants.KeyCode[hid_key_code.name]
    self.logger.info("[REF] Press HID key %s", hid_key_code.name)
    self.ref_hid_device.send_data(
        bytes([0x01, 0x00, 0x00, hid_key, 0x00, 0x00, 0x00, 0x00, 0x00])
    )
    self.logger.info("[DUT] Wait for key %s down", android_key_code.name)
    await dut_input_cb.wait_for_event(
        bl4a_api.KeyEvent(
            key_code=android_key_code, action=android_constants.KeyAction.DOWN
        )
    )

    self.logger.info("[REF] Release HID key %s", hid_key_code.name)

    self.logger.info("[DUT] Wait for key %s up", android_key_code.name)
    self.ref_hid_device.send_data(
        bytes([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
    )
    await dut_input_cb.wait_for_event(
        bl4a_api.KeyEvent(
            key_code=android_key_code, action=android_constants.KeyAction.UP
        )
    )

Tests the HID mouse click.

Test steps
  1. Leverage the test_connect() to establish the connection.
  2. Press primary button and wait for button press.
  3. Release primary button and wait for button down.
Source code in navi/tests/smoke/hid_test.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
async def test_mouse_click(self) -> None:
  """Tests the HID mouse click.

  Test steps:
    1. Leverage the test_connect() to establish the connection.
    2. Press primary button and wait for button press.
    3. Release primary button and wait for button down.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

  self.logger.info("[REF] Press Primary button")
  hid_report = struct.pack("<BBhhB", 0x02, 0x01, 0, 0, 0)
  self.ref_hid_device.send_data(hid_report)

  self.logger.info("[DUT] Wait for button press")
  event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
  self.assertEqual(event.action, android_constants.MotionAction.BUTTON_PRESS)

  self.logger.info("[REF] Release Primary button")
  hid_report = struct.pack("<BBhhB", 0x02, 0x00, 0, 0, 0)
  self.ref_hid_device.send_data(hid_report)

  self.logger.info("[DUT] Wait for button down")
  event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
  self.assertEqual(
      event.action, android_constants.MotionAction.BUTTON_RELEASE
  )

Tests the HID mouse movement.

Test steps
  1. Leverage the test_connect() to establish the connection.
  2. Move on X axis and wait for hover movement.
  3. Move on Y axis and wait for hover movement.
Source code in navi/tests/smoke/hid_test.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
async def test_mouse_movement(self) -> None:
  """Tests the HID mouse movement.

  Test steps:
    1. Leverage the test_connect() to establish the connection.
    2. Move on X axis and wait for hover movement.
    3. Move on Y axis and wait for hover movement.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(_PREPARE_INPUT_ACTIVITY_TIMEOUT_SECONDS)

  self.logger.info("[REF] Move on X axis")
  hid_report = struct.pack("<BBhhB", 0x02, 0, 1, 0, 0)
  self.ref_hid_device.send_data(hid_report)

  self.logger.info("[DUT] Wait for hover movement")
  await dut_input_cb.wait_for_event(
      bl4a_api.MotionEvent,
      lambda e: e.action
      in (
          android_constants.MotionAction.HOVER_MOVE,
          android_constants.MotionAction.HOVER_ENTER,
          android_constants.MotionAction.HOVER_EXIT,
      ),
  )
  # Clear all events.
  dut_input_cb.get_all_events(bl4a_api.MotionEvent)

  self.logger.info("[REF] Move on Y axis")
  hid_report = struct.pack("<BBhhB", 0x02, 0, 0, 1, 0)
  self.ref_hid_device.send_data(hid_report)

  self.logger.info("[DUT] Wait for hover movement")
  await dut_input_cb.wait_for_event(
      bl4a_api.MotionEvent,
      lambda e: e.action
      in (
          android_constants.MotionAction.HOVER_MOVE,
          android_constants.MotionAction.HOVER_ENTER,
          android_constants.MotionAction.HOVER_EXIT,
      ),
  )

Tests reconnecting the HID connection with the background scanner.

Test steps
  1. Pair with REF.
  2. Terminate the connection.
  3. Connect HID from REF.
Source code in navi/tests/smoke/hid_test.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
async def test_reconnect(self) -> None:
  """Tests reconnecting the HID connection with the background scanner.

  Test steps:
    1. Pair with REF.
    2. Terminate the connection.
    3. Connect HID from REF.
  """
  await self.test_connect()

  ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
      hci.Address(self.dut.address)
  )
  assert ref_dut_acl is not None
  with self.dut.bl4a.register_callback(
      bl4a_api.Module.ADAPTER
  ) as dut_adapter_cb:
    self.logger.info("[REF] Disconnect")
    await ref_dut_acl.disconnect()
    await dut_adapter_cb.wait_for_event(bl4a_api.AclDisconnected)

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.HID_HOST
  ) as dut_hid_cb:
    self.logger.info("[REF] Connect ACL")
    ref_dut_acl = await self.ref.device.connect(
        self.dut.address,
        transport=core.BT_BR_EDR_TRANSPORT,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

    self.logger.info("[REF] Encrypt")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_dut_acl.authenticate()
      await ref_dut_acl.encrypt()

    self.logger.info("[REF] Connect HID")
    self.ref_hid_device = await hid.DeviceProtocol.connect(ref_dut_acl)

    self.logger.info("[DUT] Wait for connected")
    await dut_hid_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/hogp_test.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
class HogpTest(navi_test_base.TwoDevicesTestBase):
  ref_hogp_service: gatt.Service
  ref_keyboard_input_report_characteristic: gatt.Characteristic
  ref_keyboard_output_report_characteristic: gatt.Characteristic
  ref_mouse_input_report_characteristic: gatt.Characteristic

  def _setup_hid_service(self) -> None:
    self.ref_keyboard_input_report_characteristic = gatt.Characteristic(
        gatt.GATT_REPORT_CHARACTERISTIC,
        gatt.Characteristic.Properties.READ
        | gatt.Characteristic.Properties.WRITE
        | gatt.Characteristic.Properties.NOTIFY,
        gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
        bytes(8),
        [
            gatt.Descriptor(
                gatt.GATT_REPORT_REFERENCE_DESCRIPTOR,
                gatt.Descriptor.READABLE,
                bytes([0x01, _HidReportType.INPUT.value]),
            )
        ],
    )

    self.ref_keyboard_output_report_characteristic = gatt.Characteristic(
        gatt.GATT_REPORT_CHARACTERISTIC,
        gatt.Characteristic.Properties.READ
        | gatt.Characteristic.Properties.WRITE
        | gatt.Characteristic.WRITE_WITHOUT_RESPONSE,
        gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
        bytes([0]),
        [
            gatt.Descriptor(
                gatt.GATT_REPORT_REFERENCE_DESCRIPTOR,
                gatt.Descriptor.READABLE,
                bytes([0x01, _HidReportType.OUTPUT.value]),
            )
        ],
    )
    self.ref_mouse_input_report_characteristic = gatt.Characteristic(
        gatt.GATT_REPORT_CHARACTERISTIC,
        gatt.Characteristic.Properties.READ
        | gatt.Characteristic.Properties.WRITE
        | gatt.Characteristic.Properties.NOTIFY,
        gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
        bytes(6),
        [
            gatt.Descriptor(
                gatt.GATT_REPORT_REFERENCE_DESCRIPTOR,
                gatt.Descriptor.READABLE,
                bytes([0x02, _HidReportType.INPUT.value]),
            )
        ],
    )
    self.ref_hogp_service = gatt.Service(
        gatt.GATT_HUMAN_INTERFACE_DEVICE_SERVICE,
        [
            gatt.Characteristic(
                gatt.GATT_PROTOCOL_MODE_CHARACTERISTIC,
                gatt.Characteristic.Properties.READ,
                gatt.Characteristic.READABLE,
                bytes([_HidReportProtocol.REPORT.value]),
            ),
            gatt.Characteristic(
                gatt.GATT_HID_INFORMATION_CHARACTERISTIC,
                gatt.Characteristic.Properties.READ,
                gatt.Characteristic.READABLE,
                # bcdHID=1.1, bCountryCode=0x00,
                # Flags=RemoteWake|NormallyConnectable
                bytes([0x11, 0x01, 0x00, 0x03]),
            ),
            gatt.Characteristic(
                gatt.GATT_HID_CONTROL_POINT_CHARACTERISTIC,
                gatt.Characteristic.WRITE_WITHOUT_RESPONSE,
                gatt.Characteristic.WRITEABLE,
            ),
            gatt.Characteristic(
                gatt.GATT_REPORT_MAP_CHARACTERISTIC,
                gatt.Characteristic.Properties.READ,
                gatt.Characteristic.READABLE,
                HID_REPORT_MAP,
            ),
            self.ref_keyboard_input_report_characteristic,
            self.ref_keyboard_output_report_characteristic,
            self.ref_mouse_input_report_characteristic,
        ],
    )
    self.ref.device.add_service(self.ref_hogp_service)

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.device.adb.getprop(PROPERTY_HID_HOST_SUPPORTED) != "true":
      raise signals.TestAbortClass("HID host is not supported on DUT")

    # Stay awake during the test.
    self.dut.shell("svc power stayon true")
    # Dismiss the keyguard.
    self.dut.shell("wm dismiss-keyguard")

  @override
  async def async_teardown_class(self) -> None:
    await super().async_teardown_class()
    # Stop staying awake during the test.
    self.dut.shell("svc power stayon false")

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()

  async def test_connect(self) -> None:
    """Tests establishing the HID connection from DUT to REF.

    Test steps:
      1. Establish the HID connection between DUT and REF.
      2. Verify the HID connection is established.
    """
    self._setup_hid_service()
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.HID_HOST
    ) as dut_hid_cb:
      self.logger.info("[DUT] Pair with REF")
      await self.le_connect_and_pair(
          hci.OwnAddressType.RANDOM, connect_profiles=True
      )
      self.logger.info("[DUT] Wait for HID connected")
      await dut_hid_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

  async def test_reconnect(self) -> None:
    """Tests reconnecting the HID connection with the background scanner.

    Test steps:
      1. Pair with REF.
      2. Terminate the connection.
      3. Start advertising on REF.
      4. Verify the HID connection is re-established by the background scanner.
    """
    await self.test_connect()

    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address)
    )
    assert ref_dut_acl is not None
    self.logger.info("[REF] Disconnect")
    await ref_dut_acl.disconnect()

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.HID_HOST
    ) as dut_hid_cb:
      self.logger.info("[REF] Restart advertising")
      await self.ref.device.start_advertising(
          own_address_type=hci.OwnAddressType.RANDOM,
      )
      self.logger.info("[DUT] Wait for connected")
      await dut_hid_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

  async def test_keyboard_input(self) -> None:
    """Tests the HID keyboard input.

    Test steps:
      1. Establish the HID connection between DUT and REF.
      2. Press each key on the keyboard and verify the key down and up events
         on DUT.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()
    report_characteristic = self.ref_keyboard_input_report_characteristic

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(0.5)

    for hid_key in range(
        constants.UsbHidKeyCode.A, constants.UsbHidKeyCode.Z + 1
    ):
      hid_key_code = constants.UsbHidKeyCode(hid_key)
      android_key_code = android_constants.KeyCode[hid_key_code.name]
      self.logger.info("[REF] Press HID key %s", hid_key_code.name)
      report_characteristic.value = bytes(
          [0x00, 0x00, hid_key, 0x00, 0x00, 0x00, 0x00, 0x00]
      )
      await self.ref.device.notify_subscribers(report_characteristic)
      self.logger.info("[DUT] Wait for key %s down", android_key_code.name)
      await dut_input_cb.wait_for_event(
          bl4a_api.KeyEvent(
              key_code=android_key_code, action=android_constants.KeyAction.DOWN
          )
      )

      self.logger.info("[REF] Release HID key %s", hid_key_code.name)
      report_characteristic.value = bytes(8)

      self.logger.info("[DUT] Wait for key %s up", android_key_code.name)
      await self.ref.device.notify_subscribers(report_characteristic)
      await dut_input_cb.wait_for_event(
          bl4a_api.KeyEvent(
              key_code=android_key_code, action=android_constants.KeyAction.UP
          )
      )

  async def test_mouse_click(self) -> None:
    """Tests the HID mouse click.

    Test steps:
      1. Leverage the test_connect() to establish the connection.
      2. Press primary button and wait for button press.
      3. Release primary button and wait for button down.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()
    report_characteristic = self.ref_mouse_input_report_characteristic

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(0.5)

    self.logger.info("[REF] Press Primary button")
    report_characteristic.value = struct.pack("<BhhB", 0x01, 0, 0, 0)
    await self.ref.device.notify_subscribers(report_characteristic)

    self.logger.info("[DUT] Wait for button press")
    event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
    self.assertEqual(event.action, android_constants.MotionAction.BUTTON_PRESS)

    self.logger.info("[REF] Release Primary button")
    report_characteristic.value = struct.pack("<BhhB", 0x00, 0, 0, 0)
    await self.ref.device.notify_subscribers(report_characteristic)

    self.logger.info("[DUT] Wait for button down")
    event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
    self.assertEqual(
        event.action, android_constants.MotionAction.BUTTON_RELEASE
    )

  async def test_mouse_movement(self) -> None:
    """Tests the HID mouse movement.

    Test steps:
      1. Leverage the test_connect() to establish the connection.
      2. Move on X axis and wait for hover movement.
      3. Move on Y axis and wait for hover movement.
    """
    # Leverage the test_connect() to establish the connection.
    await self.test_connect()
    report_characteristic = self.ref_mouse_input_report_characteristic

    dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
    self.test_case_context.push(dut_input_cb)

    # Wait for the InputActivity to be ready.
    await asyncio.sleep(0.5)

    self.logger.info("[REF] Move on X axis")
    report_characteristic.value = struct.pack("<BhhB", 0, 1, 0, 0)
    await self.ref.device.notify_subscribers(report_characteristic)

    self.logger.info("[DUT] Wait for hover movement")
    await dut_input_cb.wait_for_event(
        bl4a_api.MotionEvent,
        lambda e: e.action
        in (
            android_constants.MotionAction.HOVER_MOVE,
            android_constants.MotionAction.HOVER_ENTER,
            android_constants.MotionAction.HOVER_EXIT,
        ),
    )
    # Clear all events.
    dut_input_cb.get_all_events(bl4a_api.MotionEvent)

    self.logger.info("[REF] Move on Y axis")
    report_characteristic.value = struct.pack("<BhhB", 0x00, 0, 1, 0)
    await self.ref.device.notify_subscribers(report_characteristic)

    self.logger.info("[DUT] Wait for hover movement")
    await dut_input_cb.wait_for_event(
        bl4a_api.MotionEvent,
        lambda e: e.action
        in (
            android_constants.MotionAction.HOVER_MOVE,
            android_constants.MotionAction.HOVER_ENTER,
            android_constants.MotionAction.HOVER_EXIT,
        ),
    )

Tests establishing the HID connection from DUT to REF.

Test steps
  1. Establish the HID connection between DUT and REF.
  2. Verify the HID connection is established.
Source code in navi/tests/smoke/hogp_test.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
async def test_connect(self) -> None:
  """Tests establishing the HID connection from DUT to REF.

  Test steps:
    1. Establish the HID connection between DUT and REF.
    2. Verify the HID connection is established.
  """
  self._setup_hid_service()
  with self.dut.bl4a.register_callback(
      bl4a_api.Module.HID_HOST
  ) as dut_hid_cb:
    self.logger.info("[DUT] Pair with REF")
    await self.le_connect_and_pair(
        hci.OwnAddressType.RANDOM, connect_profiles=True
    )
    self.logger.info("[DUT] Wait for HID connected")
    await dut_hid_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.random_address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

Tests the HID keyboard input.

Test steps
  1. Establish the HID connection between DUT and REF.
  2. Press each key on the keyboard and verify the key down and up events on DUT.
Source code in navi/tests/smoke/hogp_test.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
async def test_keyboard_input(self) -> None:
  """Tests the HID keyboard input.

  Test steps:
    1. Establish the HID connection between DUT and REF.
    2. Press each key on the keyboard and verify the key down and up events
       on DUT.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()
  report_characteristic = self.ref_keyboard_input_report_characteristic

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(0.5)

  for hid_key in range(
      constants.UsbHidKeyCode.A, constants.UsbHidKeyCode.Z + 1
  ):
    hid_key_code = constants.UsbHidKeyCode(hid_key)
    android_key_code = android_constants.KeyCode[hid_key_code.name]
    self.logger.info("[REF] Press HID key %s", hid_key_code.name)
    report_characteristic.value = bytes(
        [0x00, 0x00, hid_key, 0x00, 0x00, 0x00, 0x00, 0x00]
    )
    await self.ref.device.notify_subscribers(report_characteristic)
    self.logger.info("[DUT] Wait for key %s down", android_key_code.name)
    await dut_input_cb.wait_for_event(
        bl4a_api.KeyEvent(
            key_code=android_key_code, action=android_constants.KeyAction.DOWN
        )
    )

    self.logger.info("[REF] Release HID key %s", hid_key_code.name)
    report_characteristic.value = bytes(8)

    self.logger.info("[DUT] Wait for key %s up", android_key_code.name)
    await self.ref.device.notify_subscribers(report_characteristic)
    await dut_input_cb.wait_for_event(
        bl4a_api.KeyEvent(
            key_code=android_key_code, action=android_constants.KeyAction.UP
        )
    )

Tests the HID mouse click.

Test steps
  1. Leverage the test_connect() to establish the connection.
  2. Press primary button and wait for button press.
  3. Release primary button and wait for button down.
Source code in navi/tests/smoke/hogp_test.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
async def test_mouse_click(self) -> None:
  """Tests the HID mouse click.

  Test steps:
    1. Leverage the test_connect() to establish the connection.
    2. Press primary button and wait for button press.
    3. Release primary button and wait for button down.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()
  report_characteristic = self.ref_mouse_input_report_characteristic

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(0.5)

  self.logger.info("[REF] Press Primary button")
  report_characteristic.value = struct.pack("<BhhB", 0x01, 0, 0, 0)
  await self.ref.device.notify_subscribers(report_characteristic)

  self.logger.info("[DUT] Wait for button press")
  event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
  self.assertEqual(event.action, android_constants.MotionAction.BUTTON_PRESS)

  self.logger.info("[REF] Release Primary button")
  report_characteristic.value = struct.pack("<BhhB", 0x00, 0, 0, 0)
  await self.ref.device.notify_subscribers(report_characteristic)

  self.logger.info("[DUT] Wait for button down")
  event = await dut_input_cb.wait_for_event(bl4a_api.MotionEvent)
  self.assertEqual(
      event.action, android_constants.MotionAction.BUTTON_RELEASE
  )

Tests the HID mouse movement.

Test steps
  1. Leverage the test_connect() to establish the connection.
  2. Move on X axis and wait for hover movement.
  3. Move on Y axis and wait for hover movement.
Source code in navi/tests/smoke/hogp_test.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
async def test_mouse_movement(self) -> None:
  """Tests the HID mouse movement.

  Test steps:
    1. Leverage the test_connect() to establish the connection.
    2. Move on X axis and wait for hover movement.
    3. Move on Y axis and wait for hover movement.
  """
  # Leverage the test_connect() to establish the connection.
  await self.test_connect()
  report_characteristic = self.ref_mouse_input_report_characteristic

  dut_input_cb = self.dut.bl4a.register_callback(bl4a_api.Module.INPUT)
  self.test_case_context.push(dut_input_cb)

  # Wait for the InputActivity to be ready.
  await asyncio.sleep(0.5)

  self.logger.info("[REF] Move on X axis")
  report_characteristic.value = struct.pack("<BhhB", 0, 1, 0, 0)
  await self.ref.device.notify_subscribers(report_characteristic)

  self.logger.info("[DUT] Wait for hover movement")
  await dut_input_cb.wait_for_event(
      bl4a_api.MotionEvent,
      lambda e: e.action
      in (
          android_constants.MotionAction.HOVER_MOVE,
          android_constants.MotionAction.HOVER_ENTER,
          android_constants.MotionAction.HOVER_EXIT,
      ),
  )
  # Clear all events.
  dut_input_cb.get_all_events(bl4a_api.MotionEvent)

  self.logger.info("[REF] Move on Y axis")
  report_characteristic.value = struct.pack("<BhhB", 0x00, 0, 1, 0)
  await self.ref.device.notify_subscribers(report_characteristic)

  self.logger.info("[DUT] Wait for hover movement")
  await dut_input_cb.wait_for_event(
      bl4a_api.MotionEvent,
      lambda e: e.action
      in (
          android_constants.MotionAction.HOVER_MOVE,
          android_constants.MotionAction.HOVER_ENTER,
          android_constants.MotionAction.HOVER_EXIT,
      ),
  )

Tests reconnecting the HID connection with the background scanner.

Test steps
  1. Pair with REF.
  2. Terminate the connection.
  3. Start advertising on REF.
  4. Verify the HID connection is re-established by the background scanner.
Source code in navi/tests/smoke/hogp_test.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
async def test_reconnect(self) -> None:
  """Tests reconnecting the HID connection with the background scanner.

  Test steps:
    1. Pair with REF.
    2. Terminate the connection.
    3. Start advertising on REF.
    4. Verify the HID connection is re-established by the background scanner.
  """
  await self.test_connect()

  ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
      hci.Address(self.dut.address)
  )
  assert ref_dut_acl is not None
  self.logger.info("[REF] Disconnect")
  await ref_dut_acl.disconnect()

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.HID_HOST
  ) as dut_hid_cb:
    self.logger.info("[REF] Restart advertising")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM,
    )
    self.logger.info("[DUT] Wait for connected")
    await dut_hid_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.random_address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/l2cap_test.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
class L2capTest(navi_test_base.TwoDevicesTestBase):
  _ANDROID_AUTO_ALLOCATE_PSM = -2

  @override
  def on_fail(self, record: records.TestResultRecord) -> None:
    super().on_fail(record)
    self.dut.reload_snippet()

  async def _setup_le_pairing(self) -> None:
    # Terminate ACL connection after pairing.
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      await self.le_connect_and_pair(hci.OwnAddressType.RANDOM)
      self.logger.info("[DUT] Wait for disconnected.")
      ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          core.BT_LE_TRANSPORT,
      )
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        if ref_dut_acl:
          with contextlib.suppress(hci.HCI_StatusError):
            await ref_dut_acl.disconnect()
        await self.ref.device.power_off()
        await self.ref.device.power_on()
      await dut_cb.wait_for_event(bl4a_api.AclDisconnected)

  async def _test_transmission(
      self,
      ref_dut_l2cap_channel: l2cap.LeCreditBasedChannel,
      dut_ref_l2cap_channel: bl4a_api.L2capChannel,
  ) -> None:
    # Store received SDUs in queue.
    ref_sdu_rx_queue = asyncio.Queue[bytes]()
    ref_dut_l2cap_channel.sink = ref_sdu_rx_queue.put_nowait

    self.logger.info("Start sending data from REF to DUT")
    ref_dut_l2cap_channel.write(_TEST_DATA)
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      data_read = await dut_ref_l2cap_channel.read(len(_TEST_DATA))
    self.assertEqual(data_read, _TEST_DATA)

    async def ref_rx_task() -> bytearray:
      data_read = bytearray()
      while len(data_read) < len(_TEST_DATA):
        data_read += await ref_sdu_rx_queue.get()
      return data_read

    self.logger.info("Start sending data from DUT to REF")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      _, data_read = await asyncio.gather(
          dut_ref_l2cap_channel.write(_TEST_DATA),
          ref_dut_l2cap_channel.connection.abort_on(
              "disconnection", ref_rx_task()
          ),
      )
      self.assertEqual(data_read, _TEST_DATA)

  @navi_test_base.parameterized(Variant.SECURE, Variant.INSECURE)
  @navi_test_base.retry(3)
  async def test_incoming_connection(self, variant: Variant) -> None:
    """Test L2CAP incoming connection, read and write.

    Typical duration: 30-60s.

    Test steps:
      1. Open L2CAP server on DUT.
      2. Start advertising on DUT.
      3. Connect ACL from REF to DUT.
      4. Connect L2CAP from REF to DUT.
      5. Transmit data from REF to DUT.
      6. Transmit data from DUT to REF.

    Args:
      variant: Whether encryption is required.
    """
    if variant == Variant.SECURE:
      await self._setup_le_pairing()

    secure = variant == Variant.SECURE

    server = self.dut.bl4a.create_l2cap_server(secure=secure)
    self.logger.info("[DUT] Listen L2CAP on PSM %d", server.psm)

    self.logger.info("[DUT] Start advertising.")
    await self.dut.bl4a.start_legacy_advertiser(
        settings=bl4a_api.LegacyAdvertiseSettings(
            own_address_type=android_constants.AddressTypeStatus.PUBLIC
        ),
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self.ref.device.connect(
        f"{self.dut.address}/P",
        transport=core.BT_LE_TRANSPORT,
        timeout=datetime.timedelta(seconds=15).total_seconds(),
        own_address_type=hci.OwnAddressType.RANDOM,
    )

    # Workaround: Request feature exchange to avoid connection failure.
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await ref_dut_acl.get_remote_le_features()

    if secure:
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        await ref_dut_acl.encrypt(True)

    self.logger.info("[REF] Connect L2CAP channel to DUT.")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      ref_dut_l2cap_channel, dut_ref_l2cap_channel = await asyncio.gather(
          ref_dut_acl.create_l2cap_channel(
              l2cap.LeCreditBasedChannelSpec(psm=server.psm)
          ),
          server.accept(),
      )

    await self._test_transmission(ref_dut_l2cap_channel, dut_ref_l2cap_channel)

  @navi_test_base.parameterized(Variant.SECURE, Variant.INSECURE)
  async def test_outgoing_connection(self, variant: Variant) -> None:
    """Test L2CAP outgoing connection, read and write.

    Typical duration: 30-60s.

    Test steps:
      1. Open L2CAP server on REF.
      2. Start advertising on REF.
      3. Connect L2CAP from REF to DUT.
      4. Transmit SDU from REF to DUT for 256 times.
      5. Transmit SDU from DUT to REF for 256 times.

    Args:
      variant: Whether encryption is required.
    """
    if variant == Variant.SECURE:
      await self._setup_le_pairing()

    secure = variant == Variant.SECURE

    ref_accept_future = asyncio.get_running_loop().create_future()
    server = self.ref.device.create_l2cap_server(
        spec=l2cap.LeCreditBasedChannelSpec(),
        handler=ref_accept_future.set_result,
    )
    self.logger.info("[REF] Listen L2CAP on PSM %d", server.psm)

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(
        own_address_type=hci.OwnAddressType.RANDOM
    )

    # On some emulator images (at least until SDK Level 35), stack may not be
    # able to connect to a random address if it's not scanned yet.
    self.logger.info("[DUT] Start scanning for REF.")
    scanner = self.dut.bl4a.start_scanning(
        scan_filter=bl4a_api.ScanFilter(
            device=self.ref.random_address,
            address_type=android_constants.AddressTypeStatus.RANDOM,
        ),
    )
    with scanner:
      self.logger.info("[DUT] Wait for scan result.")
      await scanner.wait_for_event(bl4a_api.ScanResult)

    self.logger.info("[DUT] Connect L2CAP channel to REF.")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      ref_dut_l2cap_channel, dut_ref_l2cap_channel = await asyncio.gather(
          ref_accept_future,
          self.dut.bl4a.create_l2cap_channel(
              address=self.ref.random_address,
              secure=secure,
              psm=server.psm,
              address_type=android_constants.AddressTypeStatus.RANDOM,
          ),
      )

    await self._test_transmission(ref_dut_l2cap_channel, dut_ref_l2cap_channel)

Test L2CAP incoming connection, read and write.

Typical duration: 30-60s.

Test steps
  1. Open L2CAP server on DUT.
  2. Start advertising on DUT.
  3. Connect ACL from REF to DUT.
  4. Connect L2CAP from REF to DUT.
  5. Transmit data from REF to DUT.
  6. Transmit data from DUT to REF.

Parameters:

Name Type Description Default
variant Variant

Whether encryption is required.

required
Source code in navi/tests/smoke/l2cap_test.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@navi_test_base.parameterized(Variant.SECURE, Variant.INSECURE)
@navi_test_base.retry(3)
async def test_incoming_connection(self, variant: Variant) -> None:
  """Test L2CAP incoming connection, read and write.

  Typical duration: 30-60s.

  Test steps:
    1. Open L2CAP server on DUT.
    2. Start advertising on DUT.
    3. Connect ACL from REF to DUT.
    4. Connect L2CAP from REF to DUT.
    5. Transmit data from REF to DUT.
    6. Transmit data from DUT to REF.

  Args:
    variant: Whether encryption is required.
  """
  if variant == Variant.SECURE:
    await self._setup_le_pairing()

  secure = variant == Variant.SECURE

  server = self.dut.bl4a.create_l2cap_server(secure=secure)
  self.logger.info("[DUT] Listen L2CAP on PSM %d", server.psm)

  self.logger.info("[DUT] Start advertising.")
  await self.dut.bl4a.start_legacy_advertiser(
      settings=bl4a_api.LegacyAdvertiseSettings(
          own_address_type=android_constants.AddressTypeStatus.PUBLIC
      ),
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self.ref.device.connect(
      f"{self.dut.address}/P",
      transport=core.BT_LE_TRANSPORT,
      timeout=datetime.timedelta(seconds=15).total_seconds(),
      own_address_type=hci.OwnAddressType.RANDOM,
  )

  # Workaround: Request feature exchange to avoid connection failure.
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    await ref_dut_acl.get_remote_le_features()

  if secure:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await ref_dut_acl.encrypt(True)

  self.logger.info("[REF] Connect L2CAP channel to DUT.")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    ref_dut_l2cap_channel, dut_ref_l2cap_channel = await asyncio.gather(
        ref_dut_acl.create_l2cap_channel(
            l2cap.LeCreditBasedChannelSpec(psm=server.psm)
        ),
        server.accept(),
    )

  await self._test_transmission(ref_dut_l2cap_channel, dut_ref_l2cap_channel)

Test L2CAP outgoing connection, read and write.

Typical duration: 30-60s.

Test steps
  1. Open L2CAP server on REF.
  2. Start advertising on REF.
  3. Connect L2CAP from REF to DUT.
  4. Transmit SDU from REF to DUT for 256 times.
  5. Transmit SDU from DUT to REF for 256 times.

Parameters:

Name Type Description Default
variant Variant

Whether encryption is required.

required
Source code in navi/tests/smoke/l2cap_test.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
@navi_test_base.parameterized(Variant.SECURE, Variant.INSECURE)
async def test_outgoing_connection(self, variant: Variant) -> None:
  """Test L2CAP outgoing connection, read and write.

  Typical duration: 30-60s.

  Test steps:
    1. Open L2CAP server on REF.
    2. Start advertising on REF.
    3. Connect L2CAP from REF to DUT.
    4. Transmit SDU from REF to DUT for 256 times.
    5. Transmit SDU from DUT to REF for 256 times.

  Args:
    variant: Whether encryption is required.
  """
  if variant == Variant.SECURE:
    await self._setup_le_pairing()

  secure = variant == Variant.SECURE

  ref_accept_future = asyncio.get_running_loop().create_future()
  server = self.ref.device.create_l2cap_server(
      spec=l2cap.LeCreditBasedChannelSpec(),
      handler=ref_accept_future.set_result,
  )
  self.logger.info("[REF] Listen L2CAP on PSM %d", server.psm)

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(
      own_address_type=hci.OwnAddressType.RANDOM
  )

  # On some emulator images (at least until SDK Level 35), stack may not be
  # able to connect to a random address if it's not scanned yet.
  self.logger.info("[DUT] Start scanning for REF.")
  scanner = self.dut.bl4a.start_scanning(
      scan_filter=bl4a_api.ScanFilter(
          device=self.ref.random_address,
          address_type=android_constants.AddressTypeStatus.RANDOM,
      ),
  )
  with scanner:
    self.logger.info("[DUT] Wait for scan result.")
    await scanner.wait_for_event(bl4a_api.ScanResult)

  self.logger.info("[DUT] Connect L2CAP channel to REF.")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    ref_dut_l2cap_channel, dut_ref_l2cap_channel = await asyncio.gather(
        ref_accept_future,
        self.dut.bl4a.create_l2cap_channel(
            address=self.ref.random_address,
            secure=secure,
            psm=server.psm,
            address_type=android_constants.AddressTypeStatus.RANDOM,
        ),
    )

  await self._test_transmission(ref_dut_l2cap_channel, dut_ref_l2cap_channel)

Bases: TwoDevicesTestBase

Tests for LE Audio Unicast client.

When running this test, please make sure the ref device supports CIS Peripheral.

Supported devices are: - Pixel 8 and later - Pixel 8a and later - Pixel Watch 3 and later

Unsupported devices are: - Pixel 7 and earlier - Pixel 7a and earlier - Pixel Watch 1, 2, Fitbit Ace LTE (P11)

Source code in navi/tests/smoke/le_audio_unicast_client_test.py
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
class LeAudioUnicastClientTest(navi_test_base.TwoDevicesTestBase):
  """Tests for LE Audio Unicast client.

  When running this test, please make sure the ref device supports CIS
  Peripheral.

  Supported devices are:
  - Pixel 8 and later
  - Pixel 8a and later
  - Pixel Watch 3 and later

  Unsupported devices are:
  - Pixel 7 and earlier
  - Pixel 7a and earlier
  - Pixel Watch 1, 2, Fitbit Ace LTE (P11)
  """

  ref_ascs: ascs.AudioStreamControlService
  dut_vcp_enabled: bool
  dut_mcp_enabled: bool
  dut_ccp_enabled: bool

  @classmethod
  def _default_pacs(cls) -> pacs.PublishedAudioCapabilitiesService:
    return pacs.PublishedAudioCapabilitiesService(
        supported_source_context=bap.ContextType(0xFFFF),
        available_source_context=bap.ContextType(0xFFFF),
        supported_sink_context=bap.ContextType(0xFFFF),
        available_sink_context=bap.ContextType(0xFFFF),
        sink_audio_locations=(
            bap.AudioLocation.FRONT_LEFT | bap.AudioLocation.FRONT_RIGHT
        ),
        source_audio_locations=(bap.AudioLocation.FRONT_LEFT),
        sink_pac=[
            pacs.PacRecord(
                coding_format=hci.CodingFormat(hci.CodecID.LC3),
                codec_specific_capabilities=bap.CodecSpecificCapabilities(
                    supported_sampling_frequencies=(
                        bap.SupportedSamplingFrequency.FREQ_16000
                        | bap.SupportedSamplingFrequency.FREQ_32000
                        | bap.SupportedSamplingFrequency.FREQ_48000
                    ),
                    supported_frame_durations=(
                        bap.SupportedFrameDuration.DURATION_10000_US_SUPPORTED
                    ),
                    supported_audio_channel_count=[1, 2],
                    min_octets_per_codec_frame=26,
                    max_octets_per_codec_frame=240,
                    supported_max_codec_frames_per_sdu=2,
                ),
            )
        ],
        source_pac=[
            pacs.PacRecord(
                coding_format=hci.CodingFormat(hci.CodecID.LC3),
                codec_specific_capabilities=bap.CodecSpecificCapabilities(
                    supported_sampling_frequencies=(
                        bap.SupportedSamplingFrequency.FREQ_16000
                        | bap.SupportedSamplingFrequency.FREQ_32000
                    ),
                    supported_frame_durations=(
                        bap.SupportedFrameDuration.DURATION_10000_US_SUPPORTED
                    ),
                    supported_audio_channel_count=[1],
                    min_octets_per_codec_frame=13,
                    max_octets_per_codec_frame=120,
                    supported_max_codec_frames_per_sdu=1,
                ),
            )
        ],
    )

  def _setup_unicast_server(self) -> None:
    self.ref.device.add_service(self._default_pacs())
    self.ref_ascs = ascs.AudioStreamControlService(
        self.ref.device,
        sink_ase_id=[_SINK_ASE_ID],
        source_ase_id=[_SOURCE_ASE_ID],
    )
    self.ref_vcs = vcs.VolumeControlService(volume_setting=vcs.MAX_VOLUME // 2)
    self.ref.device.add_service(self.ref_ascs)
    self.ref.device.add_service(self.ref_vcs)
    self.ref.device.add_service(
        gmap.GamingAudioService(
            gmap_role=gmap.GmapRole.UNICAST_GAME_TERMINAL,
            ugt_features=(
                gmap.UgtFeatures.UGT_SOURCE | gmap.UgtFeatures.UGT_SINK
            ),
        )
    )

  async def _prepare_paired_devices(self) -> None:
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.LE_AUDIO
    ) as dut_lea_cb:
      self.logger.info("[DUT] Pair with REF")
      await self.le_connect_and_pair(
          ref_address_type=hci.OwnAddressType.RANDOM, connect_profiles=True
      )

      self.logger.info("[DUT] Wait for LE Audio connected")
      event = await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
      self.assertEqual(event.address, self.ref.random_address)
      self.logger.info("[DUT] Wait for audio route ready")
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(self.ref.random_address)
      )

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.getprop(_AndroidProperty.BAP_UNICAST_CLIENT_ENABLED) != "true":
      raise signals.TestAbortClass("Unicast client is not enabled")

    if (
        self.dut.getprop(_AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST) != "true"
        and not self.dut.getprop(_AndroidProperty.LEAUDIO_ALLOW_LIST)
        and self.dut.getprop("ro.hardware") != "cutf_cvm"
    ):
      # Allow list will not be used in the test, but here we still check if the
      # allow list is empty to make sure DUT is ready to use LE Audio.
      raise signals.TestAbortClass(
          "Allow list is empty, DUT is probably not ready to use LE Audio."
      )

    self.ref.config.cis_enabled = True
    self.ref.device.cis_enabled = True
    self.dut_vcp_enabled = (
        self.dut.getprop(_AndroidProperty.VCP_CONTROLLER_ENABLED) == "true"
    )
    self.dut_mcp_enabled = (
        self.dut.getprop(_AndroidProperty.MCP_SERVER_ENABLED) == "true"
    )
    self.dut_ccp_enabled = (
        self.dut.getprop(_AndroidProperty.CCP_SERVER_ENABLED) == "true"
    )

    # TODO: Remove this once the bug is fixed in Bumble.
    def on_release(
        ase: ascs.AseStateMachine,
    ) -> tuple[ascs.AseResponseCode, ascs.AseReasonCode]:
      if ase.state == ascs.AseStateMachine.State.IDLE:
        return (
            ascs.AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
            ascs.AseReasonCode.NONE,
        )
      # ASE state cannot be changed to IDLE directly.
      ase.state = ase.State.RELEASING
      utils.cancel_on_event(
          ase.service.device,
          "flush",
          ase.service.device.notify_subscribers(ase, ase.value),
      )
      ase.state = ase.State.IDLE

      if ase.cis_link:
        ase.cis_link.acl_connection.cancel_on_disconnection(
            ase.cis_link.remove_data_path([ase.cis_link.Direction(ase.role)])
        )
      return (ascs.AseResponseCode.SUCCESS, ascs.AseReasonCode.NONE)

    self.test_class_context.enter_context(
        mock.patch.object(ascs.AseStateMachine, "on_release", on_release)
    )

  @override
  async def async_setup_test(self) -> None:
    # Disable the allow list to allow the connect LE Audio to Bumble.
    self.dut.setprop(_AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST, "true")
    await super().async_setup_test()
    self._setup_unicast_server()
    # Reset audio attributes to media.
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.MEDIA),
        handle_audio_focus=False,
    )
    await self._prepare_paired_devices()

  @override
  async def async_teardown_test(self) -> None:
    # Make sure audio is stopped before starting the test.
    await asyncio.to_thread(self.dut.bt.audioStop)
    # Reset to the default value.
    self.dut.bt.setHandleAudioBecomingNoisy(False)
    await super().async_teardown_test()

  def _get_sampling_frequency(
      self, ase: ascs.AseStateMachine
  ) -> bap.SamplingFrequency | None:
    """Returns the sampling frequency of the ASE."""
    if isinstance(
        codec_config := ase.codec_specific_configuration,
        bap.CodecSpecificConfiguration,
    ):
      return codec_config.sampling_frequency
    return None

  @navi_test_base.named_parameterized(
      ("active", True),
      ("passive", False),
  )
  @navi_test_base.retry(_DEFAULT_RETRY_COUNT)
  async def test_reconnect(self, is_active: bool) -> None:
    """Tests to reconnect the LE Audio Unicast server.

    Args:
      is_active: True if reconnect is actively initialized by DUT, otherwise TA
        will be used to perform the reconnection passively.
    """
    if not is_active and self.dut.device.is_emulator:
      self.skipTest(
          "b/425668688 - TA filter reconnection is not supported on rootcanal"
          " yet."
      )

    with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
      self.logger.info("[DUT] Disconnect REF")
      self.dut.bt.disconnect(self.ref.random_address)

      self.logger.info("[DUT] Wait for LE Audio disconnected")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=None),
      )

      self.logger.info("[REF] Start advertising")
      await self.ref.device.create_advertising_set(
          advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
          advertising_data=bytes(
              bap.UnicastServerAdvertisingData(
                  announcement_type=bap.AnnouncementType.GENERAL
                  if is_active
                  else bap.AnnouncementType.TARGETED,
              )
          ),
      )
      if is_active:
        self.logger.info("[DUT] Reconnect REF")
        self.dut.bt.connect(self.ref.random_address)

      self.logger.info("[DUT] Wait for LE Audio connected")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
      )

  async def test_unidirectional_audio_stream(self) -> None:
    """Tests unidirectional audio stream between DUT and REF.

    Test steps:
      1. [Optional] Wait for audio streaming to stop if it is already streaming.
      2. Start audio streaming from DUT.
      3. Wait for audio streaming to start from REF.
      4. Stop audio streaming from DUT.
      5. Wait for audio streaming to stop from REF.
    """
    sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

    self.logger.info("[DUT] Start audio streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to start",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

    # Setup audio sink.
    sink_frames = list[bytes]()
    decoder = decoder_for_ase(sink_ase) if lc3 else None

    def sink(pdu: hci.HCI_IsoDataPacket):
      if pdu.iso_sdu_fragment:
        sink_frames.append(pdu.iso_sdu_fragment)

    assert (cis_link := sink_ase.cis_link)
    cis_link.sink = sink

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info("[DUT] Stop audio streaming")
    cis_link.sink = None
    await asyncio.to_thread(self.dut.bt.audioStop)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

    if self.user_params.get(navi_test_base.RECORD_FULL_DATA):
      self.write_test_output_data("sink.lc3", b"".join(sink_frames))
    if lc3 and decoder and audio.SUPPORT_AUDIO_PROCESSING:
      pcm_format = lc3.PcmFormat.SIGNED_16
      decoded_frames = [
          decoder.decode(frame, pcm_format) for frame in sink_frames
      ]
      dominant_frequency = audio.get_dominant_frequency(
          buffer=b"".join(decoded_frames),
          format="pcm",
          sample_width=pcm_format.sample_width,
          frame_rate=_DEFAULT_FRAME_RATE,
          channels=decoder.num_channels,
      )
      self.logger.info("dominant_frequency: %.2f", dominant_frequency)
      self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

  async def test_gaming_context(self) -> None:
    """Tests streaming with gaming context.

    Test steps:
      1. [Optional] Wait for audio streaming to stop if it is already streaming.
      2. Start audio streaming from DUT with gaming context and put a call on
      DUT.
      3. Wait for audio streaming to start from REF.
      4. Stop audio streaming from DUT and end the call.
      5. Wait for audio streaming to stop from REF.
    """
    sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]
    source_ase = self.ref_ascs.ase_state_machines[_SOURCE_ASE_ID]
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.GAME),
        handle_audio_focus=False,
    )

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      for ase in self.ref_ascs.ase_state_machines.values():
        await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

    self.logger.info("[DUT] Put a VoIP call")
    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    )
    self.test_case_context.push(call)

    self.logger.info("[DUT] Start audio streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for sink ASE to start",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

    self.logger.info("[DUT] Start audio recording")
    recorder = await asyncio.to_thread(
        lambda: self.dut.bl4a.start_audio_recording(
            _RECORDING_PATH,
            source=bl4a_api.AudioRecorder.Source.VOICE_PERFORMANCE,
        )
    )
    self.test_case_context.push(recorder)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for source ASE to start",
    ):
      await _wait_for_ase_state(
          source_ase, ascs.AseStateMachine.State.STREAMING
      )

    # Check codec configuration.
    sink_freq = self._get_sampling_frequency(sink_ase)
    source_freq = self._get_sampling_frequency(source_ase)
    self.logger.info("sink_freq: %r, source_freq: %r", sink_freq, source_freq)

    if self.dut.getprop(_AndroidProperty.GMAP_ENABLED) == "true":
      # Asymmetric configuration is enabled with GMAP.
      expected_sink_freq = bap.SamplingFrequency.FREQ_48000
    else:
      expected_sink_freq = bap.SamplingFrequency.FREQ_32000
    self.assertEqual(sink_freq, expected_sink_freq)
    self.assertEqual(source_freq, bap.SamplingFrequency.FREQ_32000)

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info("[DUT] Stop audio streaming")
    await asyncio.to_thread(self.dut.bt.audioStop)
    recorder.close()
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      for ase in self.ref_ascs.ase_state_machines.values():
        await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

  async def test_bidirectional_audio_stream(self) -> None:
    """Tests bidirectional audio stream between DUT and REF.

    Test steps:
      1. [Optional] Wait for audio streaming to stop if it is already streaming.
      2. Put a call on DUT to make conversational audio context.
      3. Start audio streaming from DUT.
      4. Wait for audio streaming to start from REF.
      5. Stop audio streaming from DUT.
      6. Wait for audio streaming to stop from REF.
    """
    dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
    self.test_case_context.push(dut_telecom_cb)
    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    )
    sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]
    source_ase = self.ref_ascs.ase_state_machines[_SOURCE_ASE_ID]
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(
            usage=bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION,
            content_type=bl4a_api.AudioAttributes.ContentType.SPEECH,
        ),
        handle_audio_focus=False,
    )

    with call:
      await dut_telecom_cb.wait_for_event(
          bl4a_api.CallStateChanged,
          lambda e: (e.state in (_CallState.CONNECTING, _CallState.DIALING)),
      )

      # Make sure audio is not streaming.
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for audio to stop",
      ):
        for ase in self.ref_ascs.ase_state_machines.values():
          await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

      self.logger.info("[DUT] Start audio streaming")
      await asyncio.to_thread(self.dut.bt.audioPlaySine)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for sink ASE to start",
      ):
        await _wait_for_ase_state(
            sink_ase, ascs.AseStateMachine.State.STREAMING
        )

      self.logger.info("[DUT] Start audio recording")
      recorder = await asyncio.to_thread(
          lambda: self.dut.bl4a.start_audio_recording(
              _RECORDING_PATH,
              source=bl4a_api.AudioRecorder.Source.VOICE_COMMUNICATION,
          )
      )
      self.test_case_context.push(recorder)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for source ASE to start",
      ):
        await _wait_for_ase_state(
            source_ase, ascs.AseStateMachine.State.STREAMING
        )

      # Setup audio sink.
      sink_frames = list[bytes]()
      decoder = decoder_for_ase(sink_ase) if lc3 else None

      def sink(pdu: hci.HCI_IsoDataPacket):
        if pdu.iso_sdu_fragment:
          sink_frames.append(pdu.iso_sdu_fragment)

      assert (cis_link := sink_ase.cis_link)
      cis_link.sink = sink

      # Streaming for 1 second.
      await asyncio.sleep(_STREAMING_TIME_SECONDS)

      self.logger.info("[DUT] Stop audio streaming")
      cis_link.sink = None
      await asyncio.to_thread(self.dut.bt.audioStop)
      recorder.close()

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      for ase in self.ref_ascs.ase_state_machines.values():
        await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

    if self.user_params.get(navi_test_base.RECORD_FULL_DATA):
      self.write_test_output_data("sink.lc3", b"".join(sink_frames))
    if lc3 and decoder and audio.SUPPORT_AUDIO_PROCESSING:
      pcm_format = lc3.PcmFormat.SIGNED_16
      decoded_frames = [
          decoder.decode(frame, pcm_format) for frame in sink_frames
      ]
      dominant_frequency = audio.get_dominant_frequency(
          buffer=b"".join(decoded_frames),
          format="pcm",
          sample_width=pcm_format.sample_width,
          frame_rate=_DEFAULT_FRAME_RATE,
          channels=decoder.num_channels,
      )
      self.logger.info("dominant_frequency: %.2f", dominant_frequency)
      self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

  async def test_reconnect_during_call(self) -> None:
    """Tests reconnecting during a call. Call audio should be routed to Unicast.

    Test steps:
      1. Disconnect REF.
      2. Put a call on DUT.
      3. Reconnect REF.
      4. Wait for audio streaming to start from REF.
    """
    if self.dut.device.is_emulator:
      self.skipTest(
          "b/425668688 - TA filter reconnection is not supported on rootcanal"
          " yet."
      )

    with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
      self.logger.info("[DUT] Disconnect REF")
      self.dut.bt.disconnect(self.ref.random_address)

      self.logger.info("[DUT] Wait for LE Audio disconnected")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=None),
      )

    with contextlib.ExitStack() as stack:
      dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
      stack.enter_context(dut_telecom_cb)
      self.logger.info("[DUT] Put a call")
      call = self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          constants.Direction.OUTGOING,
      )
      stack.enter_context(call)
      await dut_telecom_cb.wait_for_event(
          bl4a_api.CallStateChanged,
          lambda e: (e.state in (_CallState.CONNECTING, _CallState.DIALING)),
      )
      # Start audio streaming from DUT.
      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
      self.dut.bt.audioPlaySine()
      recorder = await asyncio.to_thread(
          lambda: self.dut.bl4a.start_audio_recording(
              _RECORDING_PATH,
              source=bl4a_api.AudioRecorder.Source.VOICE_COMMUNICATION,
          )
      )
      stack.enter_context(recorder)

      dut_leaudio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO)
      stack.enter_context(dut_leaudio_cb)

      self.logger.info("[REF] Start advertising")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await self.ref.device.create_advertising_set(
            advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
            advertising_data=bytes(
                bap.UnicastServerAdvertisingData(
                    announcement_type=bap.AnnouncementType.TARGETED
                )
            ),
        )
      self.logger.info("[DUT] Wait for LE Audio connected")
      await dut_leaudio_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
      )

      self.logger.info("[REF] Wait for streaming to start")
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for audio to start",
      ):
        for ase in self.ref_ascs.ase_state_machines.values():
          await _wait_for_ase_state(ase, ascs.AseStateMachine.State.STREAMING)

  async def test_reconfiguration(self) -> None:
    """Tests reconfiguration from media to conversational.

    Test steps:
      1. [Optional] Wait for audio streaming to stop if it is already streaming.
      2. Start audio streaming from DUT.
      3. Wait for audio streaming to start from REF.
      4. Put a call on DUT to trigger reconfiguration.
      5. Wait for ASE to be reconfigured.
    """
    sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

    self.logger.info("[DUT] Start audio streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to start",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)
    get_audio_context = lambda: next(
        entry
        for entry in sink_ase.metadata.entries
        if entry.tag == le_audio.Metadata.Tag.STREAMING_AUDIO_CONTEXTS
    )
    context_type = struct.unpack_from("<H", get_audio_context().data)[0]
    self.assertNotEqual(context_type, bap.ContextType.PROHIBITED)
    self.assertFalse(context_type & bap.ContextType.CONVERSATIONAL)

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    )
    with call:
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[DUT] Wait for ASE to be released",
      ):
        await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[DUT] Wait for ASE to be reconfigured",
      ):
        await _wait_for_ase_state(
            sink_ase, ascs.AseStateMachine.State.STREAMING
        )
      context_type = struct.unpack_from("<H", get_audio_context().data)[0]
      self.assertTrue(context_type & bap.ContextType.CONVERSATIONAL)

  async def test_volume_initialization(self) -> None:
    """Makes sure DUT sets the volume correctly after connecting to REF."""
    if not self.dut_vcp_enabled:
      self.skipTest("VCP is not enabled on DUT")

    # When the flag is enabled, DUT's volume will be applied to REF.
    if self.dut.bluetooth_flags.get("vcp_device_volume_api_improvements", True):
      vcs_volume = pyee_extensions.EventTriggeredValueObserver[int](
          self.ref_vcs,
          self.ref_vcs.EVENT_VOLUME_STATE_CHANGE,
          lambda: self.ref_vcs.volume_setting,
      )
      ref_expected_volume = decimal.Decimal(
          self.dut.bt.getVolume(_StreamType.MUSIC)
          / self.dut.bt.getMaxVolume(_StreamType.MUSIC)
          * vcs.MAX_VOLUME
      ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          "[REF] Wait for volume to be synced with DUT",
      ):
        await vcs_volume.wait_for_target_value(int(ref_expected_volume))
    else:
      dut_expected_volume = decimal.Decimal(
          self.ref_vcs.volume_setting
          / vcs.MAX_VOLUME
          * self.dut.bt.getMaxVolume(_StreamType.MUSIC)
      ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
      with (
          self.dut.bl4a.register_callback(
              bl4a_api.Module.AUDIO
          ) as dut_audio_cb,
      ):
        if self.dut.bt.getVolume(_StreamType.MUSIC) != dut_expected_volume:
          self.logger.info("[DUT] Wait for volume to be synced with REF")
          await dut_audio_cb.wait_for_event(
              event=bl4a_api.VolumeChanged(
                  stream_type=_StreamType.MUSIC,
                  volume_value=int(dut_expected_volume),
              ),
          )

  @navi_test_base.parameterized(_TestRole.DUT, _TestRole.REF)
  async def test_set_volume(self, issuer: _TestRole) -> None:
    """Tests setting volume over LEA VCP from DUT or REF.

    Test steps:
      1. Set volume from DUT or REF.
      2. Wait for the volume to be set correctly on the other device.

    Args:
      issuer: The issuer of the volume setting.
    """
    if not self.dut_vcp_enabled:
      self.skipTest("VCP is not enabled on DUT")

    dut_max_volume = self.dut.bt.getMaxVolume(_StreamType.MUSIC)
    dut_expected_volume = (self.dut.bt.getVolume(_StreamType.MUSIC) + 1) % (
        dut_max_volume + 1
    )
    ref_expected_volume = int(
        decimal.Decimal(
            dut_expected_volume / dut_max_volume * vcs.MAX_VOLUME
        ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
    )

    # DUT's VCS client might not be stable at the beginning. If we set volume
    # immediately, the volume might not be set correctly.
    await asyncio.sleep(_PREPARE_TIME_SECONDS)

    with (
        self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb,
    ):
      vcs_volume = pyee_extensions.EventTriggeredValueObserver[int](
          self.ref_vcs,
          self.ref_vcs.EVENT_VOLUME_STATE_CHANGE,
          lambda: self.ref_vcs.volume_setting,
      )
      if issuer == _TestRole.DUT:
        self.logger.info("[DUT] Set volume to %d", dut_expected_volume)
        self.dut.bt.setVolume(_StreamType.MUSIC, dut_expected_volume)
        async with self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS,
            msg="[REF] Wait for volume to be set",
        ):
          await vcs_volume.wait_for_target_value(ref_expected_volume)
      else:
        self.logger.info("[REF] Set volume to %d", ref_expected_volume)
        self.ref_vcs.volume_setting = ref_expected_volume
        await self.ref.device.notify_subscribers(self.ref_vcs.volume_state)
        await dut_audio_cb.wait_for_event(
            event=bl4a_api.VolumeChanged(
                stream_type=_StreamType.MUSIC,
                volume_value=int(dut_expected_volume),
            ),
        )

  async def test_mcp_play_pause(self) -> None:
    """Tests starting and stopping audio streaming over MCP.

    Test steps:
      1. Connect MCP.
      2. Subscribe MCP characteristics.
      3. Play audio streaming over MCP.
      4. Pause audio streaming over MCP.
    """
    if not self.dut_mcp_enabled:
      self.skipTest("MCP is not enabled on DUT")

    self.logger.info("[REF] Connect MCP")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with device.Peer(ref_dut_acl) as peer:
      ref_mcp_client = peer.create_service_proxy(
          mcp.GenericMediaControlServiceProxy
      )
      if not ref_mcp_client:
        self.fail("Failed to connect MCP")

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Subscribe MCP characteristics",
    ):
      await ref_mcp_client.subscribe_characteristics()
    assert ref_mcp_client.media_state
    media_state = await gatt_helper.MutableCharacteristicState.create(
        ref_mcp_client.media_state
    )

    # Make sure player is active but not streaming.
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    await asyncio.to_thread(self.dut.bt.audioPause)
    await asyncio.sleep(_PREPARE_TIME_SECONDS)

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Play",
    ):
      self.assertEqual(
          await ref_mcp_client.write_control_point(_McpOpcode.PLAY),
          mcp.MediaControlPointResultCode.SUCCESS,
      )
      self.logger.info("[REF] Wait for media state to be PLAY")
      await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Pause",
    ):
      self.assertEqual(
          await ref_mcp_client.write_control_point(_McpOpcode.PAUSE),
          mcp.MediaControlPointResultCode.SUCCESS,
      )
      self.logger.info("[REF] Wait for media state to be PAUSED")
      await media_state.wait_for_target_value(bytes([mcp.MediaState.PAUSED]))

  async def test_mcp_previous_next_track(self) -> None:
    """Tests moving to previous and next track over MCP.

    Test steps:
      1. Connect MCP.
      2. Subscribe MCP characteristics.
      3. Move to next track over MCP.
      4. Move to previous track over MCP.
    """
    if not self.dut_mcp_enabled:
      self.skipTest("MCP is not enabled on DUT")

    self.logger.info("[REF] Connect MCP")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with device.Peer(ref_dut_acl) as peer:
      ref_mcp_client = peer.create_service_proxy(
          mcp.GenericMediaControlServiceProxy
      )
      if not ref_mcp_client:
        self.fail("Failed to connect MCP")

    # Allow repeating to avoid the end of the track.
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    # Generate a sine wave audio file, and push it to DUT twice.
    with tempfile.NamedTemporaryFile(
        # On Windows, NamedTemporaryFile cannot be deleted if used multiple
        # times.
        delete=(sys.platform != "win32")
    ) as local_file:
      with wave.open(local_file.name, "wb") as wave_file:
        wave_file.setnchannels(1)
        wave_file.setsampwidth(2)
        wave_file.setframerate(48000)
        wave_file.writeframes(bytes(48000 * 2 * 5))  # 5 seconds.
      for i in range(2):
        self.dut.adb.push([
            local_file.name,
            f"/data/media/{self.dut.adb.current_user_id}/Music/sample-{i}.wav",
        ])

    dut_player_cb = self.dut.bl4a.register_callback(bl4a_api.Module.PLAYER)
    self.test_case_context.push(dut_player_cb)
    # Play the first track.
    self.dut.bt.audioPlayFile("/storage/self/primary/Music/sample-0.wav")
    # Add the second track to the player.
    self.dut.bt.addMediaItem("/storage/self/primary/Music/sample-1.wav")

    self.logger.info("[DUT] Wait for playback started.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=True)
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Subscribe MCP characteristics",
    ):
      await ref_mcp_client.subscribe_characteristics()

    await asyncio.sleep(_PREPARE_TIME_SECONDS)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Move to next track",
    ):
      result = await ref_mcp_client.write_control_point(_McpOpcode.NEXT_TRACK)
      self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)

    self.logger.info("[DUT] Wait for playback changed.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerMediaItemTransition(
            uri="/storage/self/primary/Music/sample-1.wav"
        ),
    )

    await asyncio.sleep(_PREPARE_TIME_SECONDS)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Move to previous track",
    ):
      result = await ref_mcp_client.write_control_point(
          _McpOpcode.PREVIOUS_TRACK
      )
      self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)

    self.logger.info("[DUT] Wait for playback changed.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerMediaItemTransition(
            uri="/storage/self/primary/Music/sample-0.wav"
        ),
    )

  async def test_mcp_fast_rewind_fast_forward(self) -> None:
    """Tests moving to previous and next track over MCP.

    Test steps:
      1. Connect MCP.
      2. Subscribe MCP characteristics.
      3. Fast forward over MCP.
      4. Fast rewind over MCP.
    """
    if not self.dut_mcp_enabled:
      self.skipTest("MCP is not enabled on DUT")

    self.logger.info("[REF] Connect MCP")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with device.Peer(ref_dut_acl) as peer:
      ref_mcp_client = peer.create_service_proxy(
          mcp.GenericMediaControlServiceProxy
      )
      if not ref_mcp_client:
        self.fail("Failed to connect MCP")

    # Push a long audio file to DUT.
    with tempfile.NamedTemporaryFile(
        # On Windows, NamedTemporaryFile cannot be deleted if used multiple
        # times.
        delete=(sys.platform != "win32")
    ) as local_file:
      with wave.open(local_file.name, "wb") as wave_file:
        wave_file.setnchannels(1)
        wave_file.setsampwidth(2)
        wave_file.setframerate(48000)
        wave_file.writeframes(bytes(48000 * 2 * 60))  # 60 seconds.
      self.dut.adb.push([
          local_file.name,
          f"/data/media/{self.dut.adb.current_user_id}/Music/sample.wav",
      ])

    await asyncio.to_thread(
        lambda: self.dut.bt.audioPlayFile(
            "/storage/self/primary/Music/sample.wav"
        )
    )

    watcher = pyee_extensions.EventWatcher()
    track_position = watcher.async_monitor(ref_mcp_client, "track_position")

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Subscribe MCP characteristics",
    ):
      await ref_mcp_client.subscribe_characteristics()

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Fast forward",
    ):
      self.assertEqual(
          await ref_mcp_client.write_control_point(_McpOpcode.FAST_FORWARD),
          mcp.MediaControlPointResultCode.SUCCESS,
      )
      self.logger.info("[REF] Wait for track position changed")
      await track_position.get()

    # Clear the track changed events.
    while not track_position.empty():
      track_position.get_nowait()

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Fast rewind",
    ):
      self.assertEqual(
          await ref_mcp_client.write_control_point(_McpOpcode.FAST_REWIND),
          mcp.MediaControlPointResultCode.SUCCESS,
      )
      self.logger.info("[REF] Wait for track position changed")
      await track_position.get()

  @navi_test_base.parameterized(_Direction.INCOMING, _Direction.OUTGOING)
  async def test_ccp_call_notifications(self, direction: _Direction) -> None:
    """Tests receiving call notifications over CCP.

    Test steps:
      1. Connect CCP.
      2. Read and subscribe CCP characteristics.
      3. Put a call from DUT, check the call info on REF.
      4. Answer the call on REF, check the call info on DUT.
      5. Terminate the call on REF, check the call info on DUT.

    Args:
      direction: The direction of the call.
    """
    if not self.dut_ccp_enabled:
      self.skipTest("CCP is not enabled on DUT")

    self.logger.info("[REF] Connect TBS")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with device.Peer(ref_dut_acl) as peer:
      ref_tbs_client = peer.create_service_proxy(
          ccp.GenericTelephoneBearerServiceProxy
      )
      if not ref_tbs_client:
        self.fail("Failed to connect TBS")

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Read and subscribe TBS characteristics",
    ):
      await ref_tbs_client.read_and_subscribe_characteristics()

    expected_call_uri = f"tel:{_CALLER_NUMBER}"
    with self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        direction,
    ) as call:
      if ref_tbs_client.call_friendly_name:
        async with self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS,
            msg="[REF] Wait for call friendly name",
        ):
          await ref_tbs_client.call_friendly_name.wait_for_target_value(
              bytes([1]) + _CALLER_NAME.encode()
          )
      expected_call_states: Sequence[ccp.CallState]
      if direction == _Direction.INCOMING:
        async with self.assert_not_timeout(
            _DEFAULT_STEP_TIMEOUT_SECONDS,
            msg="[REF] Wait for incoming call information",
        ):
          await ref_tbs_client.incoming_call.wait_for_target_value(
              bytes([1]) + expected_call_uri.encode()
          )
        expected_call_states = (ccp.CallState.INCOMING,)
        expected_call_flag = ccp.CallFlag(0)
      else:
        expected_call_states = (ccp.CallState.DIALING, ccp.CallState.ALERTING)
        expected_call_flag = ccp.CallFlag.IS_OUTGOING

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call state change",
      ):
        await ref_tbs_client.call_state.wait_for_target_value(
            lambda value: (
                len(value) >= 3
                and value[0] == 1
                and value[1] in expected_call_states
                and value[2] == expected_call_flag
            )
        )
        self.logger.info("[REF] Wait for call info change")
        await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(
            lambda value: (
                (info_list := ccp.CallInfo.parse_list(value))
                and (info_list[0].call_index == 1)
                and (info_list[0].call_state in expected_call_states)
                and (info_list[0].call_flags == expected_call_flag)
                and (info_list[0].call_uri == expected_call_uri)
            )
        )

      self.logger.info("[DUT] Answer / Activate call")
      call.answer()
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call state to be active",
      ):
        await ref_tbs_client.call_state.wait_for_target_value(
            bytes([1, ccp.CallState.ACTIVE, expected_call_flag])
        )
        self.logger.info("[REF] Wait for call info change")
        await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(
            ccp.CallInfo(
                call_index=1,
                call_state=ccp.CallState.ACTIVE,
                call_flags=expected_call_flag,
                call_uri=expected_call_uri,
            ).to_bytes()
        )

    self.logger.info("[DUT] Terminate call")
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call info removed",
    ):
      await ref_tbs_client.call_state.wait_for_target_value(b"")
      self.logger.info("[REF] Wait for call info change")
      await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(b"")

  async def test_ccp_accept_and_terminate_call(self) -> None:
    """Tests answering and terminating a call over CCP.

    Test steps:
      1. Connect CCP.
      2. Read and subscribe CCP characteristics.
      3. Put an incoming call from DUT.
      4. Accept the call on REF.
      5. Terminate the call on REF.
    """
    if not self.dut_ccp_enabled:
      self.skipTest("CCP is not enabled on DUT")

    self.logger.info("[REF] Connect TBS")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with device.Peer(ref_dut_acl) as peer:
      ref_tbs_client = peer.create_service_proxy(
          ccp.GenericTelephoneBearerServiceProxy
      )
      if not ref_tbs_client:
        self.fail("Failed to connect TBS")

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Read and subscribe TBS characteristics",
    ):
      await ref_tbs_client.read_and_subscribe_characteristics()

    expected_call_index = 1
    with (
        self.dut.bl4a.make_phone_call(
            _CALLER_NAME,
            _CALLER_NUMBER,
            _Direction.INCOMING,
        ),
        self.dut.bl4a.register_callback(
            bl4a_api.Module.TELECOM
        ) as dut_telecom_cb,
    ):
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call state change",
      ):
        await ref_tbs_client.call_state.wait_for_target_value(
            bytes(
                [expected_call_index, ccp.CallState.INCOMING, ccp.CallFlag(0)]
            )
        )

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Accept call",
      ):
        await ref_tbs_client.accept(expected_call_index)

      self.logger.info("[DUT] Wait for call to be active")
      await dut_telecom_cb.wait_for_event(
          event=bl4a_api.CallStateChanged,
          predicate=lambda e: e.state == _CallState.ACTIVE,
      )

      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Terminate call",
      ):
        await ref_tbs_client.terminate(expected_call_index)

      self.logger.info("[DUT] Wait for call to be disconnected")
      await dut_telecom_cb.wait_for_event(
          event=bl4a_api.CallStateChanged,
          predicate=lambda e: (e.state == _CallState.DISCONNECTED),
      )

  async def test_noisy_handling(self) -> None:
    """Tests enabling noisy handling, and verify the player is paused after REF disconnected.

    Test steps:
      1. Enable noisy handling.
      2. Start streaming.
      3. Disconnect from REF.
      4. Wait for player paused.
    """
    if self.dut.device.is_emulator:
      self.skipTest(
          "b/434613780 - Disconnection on streaming may cause Rootcanal crash."
      )

    # Enable audio noisy handling.
    self.dut.bt.setHandleAudioBecomingNoisy(True)

    sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for ASE state to be idle",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

    self.logger.info("[DUT] Start audio streaming")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for ASE state to be streaming",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.PLAYER
    ) as dut_player_cb:
      ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address), transport=core.BT_LE_TRANSPORT
      )
      if ref_dut_acl is None:
        self.fail("No ACL connection found?")
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Disconnect.",
      ):
        await ref_dut_acl.disconnect()

      self.logger.info("[DUT] Wait for player paused.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=False),
      )

Tests bidirectional audio stream between DUT and REF.

Test steps
  1. [Optional] Wait for audio streaming to stop if it is already streaming.
  2. Put a call on DUT to make conversational audio context.
  3. Start audio streaming from DUT.
  4. Wait for audio streaming to start from REF.
  5. Stop audio streaming from DUT.
  6. Wait for audio streaming to stop from REF.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
async def test_bidirectional_audio_stream(self) -> None:
  """Tests bidirectional audio stream between DUT and REF.

  Test steps:
    1. [Optional] Wait for audio streaming to stop if it is already streaming.
    2. Put a call on DUT to make conversational audio context.
    3. Start audio streaming from DUT.
    4. Wait for audio streaming to start from REF.
    5. Stop audio streaming from DUT.
    6. Wait for audio streaming to stop from REF.
  """
  dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
  self.test_case_context.push(dut_telecom_cb)
  call = self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.OUTGOING,
  )
  sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]
  source_ase = self.ref_ascs.ase_state_machines[_SOURCE_ASE_ID]
  self.dut.bl4a.set_audio_attributes(
      bl4a_api.AudioAttributes(
          usage=bl4a_api.AudioAttributes.Usage.VOICE_COMMUNICATION,
          content_type=bl4a_api.AudioAttributes.ContentType.SPEECH,
      ),
      handle_audio_focus=False,
  )

  with call:
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged,
        lambda e: (e.state in (_CallState.CONNECTING, _CallState.DIALING)),
    )

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to stop",
    ):
      for ase in self.ref_ascs.ase_state_machines.values():
        await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

    self.logger.info("[DUT] Start audio streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for sink ASE to start",
    ):
      await _wait_for_ase_state(
          sink_ase, ascs.AseStateMachine.State.STREAMING
      )

    self.logger.info("[DUT] Start audio recording")
    recorder = await asyncio.to_thread(
        lambda: self.dut.bl4a.start_audio_recording(
            _RECORDING_PATH,
            source=bl4a_api.AudioRecorder.Source.VOICE_COMMUNICATION,
        )
    )
    self.test_case_context.push(recorder)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for source ASE to start",
    ):
      await _wait_for_ase_state(
          source_ase, ascs.AseStateMachine.State.STREAMING
      )

    # Setup audio sink.
    sink_frames = list[bytes]()
    decoder = decoder_for_ase(sink_ase) if lc3 else None

    def sink(pdu: hci.HCI_IsoDataPacket):
      if pdu.iso_sdu_fragment:
        sink_frames.append(pdu.iso_sdu_fragment)

    assert (cis_link := sink_ase.cis_link)
    cis_link.sink = sink

    # Streaming for 1 second.
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info("[DUT] Stop audio streaming")
    cis_link.sink = None
    await asyncio.to_thread(self.dut.bt.audioStop)
    recorder.close()

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    for ase in self.ref_ascs.ase_state_machines.values():
      await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

  if self.user_params.get(navi_test_base.RECORD_FULL_DATA):
    self.write_test_output_data("sink.lc3", b"".join(sink_frames))
  if lc3 and decoder and audio.SUPPORT_AUDIO_PROCESSING:
    pcm_format = lc3.PcmFormat.SIGNED_16
    decoded_frames = [
        decoder.decode(frame, pcm_format) for frame in sink_frames
    ]
    dominant_frequency = audio.get_dominant_frequency(
        buffer=b"".join(decoded_frames),
        format="pcm",
        sample_width=pcm_format.sample_width,
        frame_rate=_DEFAULT_FRAME_RATE,
        channels=decoder.num_channels,
    )
    self.logger.info("dominant_frequency: %.2f", dominant_frequency)
    self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

Tests answering and terminating a call over CCP.

Test steps
  1. Connect CCP.
  2. Read and subscribe CCP characteristics.
  3. Put an incoming call from DUT.
  4. Accept the call on REF.
  5. Terminate the call on REF.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
async def test_ccp_accept_and_terminate_call(self) -> None:
  """Tests answering and terminating a call over CCP.

  Test steps:
    1. Connect CCP.
    2. Read and subscribe CCP characteristics.
    3. Put an incoming call from DUT.
    4. Accept the call on REF.
    5. Terminate the call on REF.
  """
  if not self.dut_ccp_enabled:
    self.skipTest("CCP is not enabled on DUT")

  self.logger.info("[REF] Connect TBS")
  ref_dut_acl = list(self.ref.device.connections.values())[0]
  async with device.Peer(ref_dut_acl) as peer:
    ref_tbs_client = peer.create_service_proxy(
        ccp.GenericTelephoneBearerServiceProxy
    )
    if not ref_tbs_client:
      self.fail("Failed to connect TBS")

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Read and subscribe TBS characteristics",
  ):
    await ref_tbs_client.read_and_subscribe_characteristics()

  expected_call_index = 1
  with (
      self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          _Direction.INCOMING,
      ),
      self.dut.bl4a.register_callback(
          bl4a_api.Module.TELECOM
      ) as dut_telecom_cb,
  ):
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call state change",
    ):
      await ref_tbs_client.call_state.wait_for_target_value(
          bytes(
              [expected_call_index, ccp.CallState.INCOMING, ccp.CallFlag(0)]
          )
      )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Accept call",
    ):
      await ref_tbs_client.accept(expected_call_index)

    self.logger.info("[DUT] Wait for call to be active")
    await dut_telecom_cb.wait_for_event(
        event=bl4a_api.CallStateChanged,
        predicate=lambda e: e.state == _CallState.ACTIVE,
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Terminate call",
    ):
      await ref_tbs_client.terminate(expected_call_index)

    self.logger.info("[DUT] Wait for call to be disconnected")
    await dut_telecom_cb.wait_for_event(
        event=bl4a_api.CallStateChanged,
        predicate=lambda e: (e.state == _CallState.DISCONNECTED),
    )

Tests receiving call notifications over CCP.

Test steps
  1. Connect CCP.
  2. Read and subscribe CCP characteristics.
  3. Put a call from DUT, check the call info on REF.
  4. Answer the call on REF, check the call info on DUT.
  5. Terminate the call on REF, check the call info on DUT.

Parameters:

Name Type Description Default
direction _Direction

The direction of the call.

required
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
@navi_test_base.parameterized(_Direction.INCOMING, _Direction.OUTGOING)
async def test_ccp_call_notifications(self, direction: _Direction) -> None:
  """Tests receiving call notifications over CCP.

  Test steps:
    1. Connect CCP.
    2. Read and subscribe CCP characteristics.
    3. Put a call from DUT, check the call info on REF.
    4. Answer the call on REF, check the call info on DUT.
    5. Terminate the call on REF, check the call info on DUT.

  Args:
    direction: The direction of the call.
  """
  if not self.dut_ccp_enabled:
    self.skipTest("CCP is not enabled on DUT")

  self.logger.info("[REF] Connect TBS")
  ref_dut_acl = list(self.ref.device.connections.values())[0]
  async with device.Peer(ref_dut_acl) as peer:
    ref_tbs_client = peer.create_service_proxy(
        ccp.GenericTelephoneBearerServiceProxy
    )
    if not ref_tbs_client:
      self.fail("Failed to connect TBS")

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Read and subscribe TBS characteristics",
  ):
    await ref_tbs_client.read_and_subscribe_characteristics()

  expected_call_uri = f"tel:{_CALLER_NUMBER}"
  with self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      direction,
  ) as call:
    if ref_tbs_client.call_friendly_name:
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for call friendly name",
      ):
        await ref_tbs_client.call_friendly_name.wait_for_target_value(
            bytes([1]) + _CALLER_NAME.encode()
        )
    expected_call_states: Sequence[ccp.CallState]
    if direction == _Direction.INCOMING:
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for incoming call information",
      ):
        await ref_tbs_client.incoming_call.wait_for_target_value(
            bytes([1]) + expected_call_uri.encode()
        )
      expected_call_states = (ccp.CallState.INCOMING,)
      expected_call_flag = ccp.CallFlag(0)
    else:
      expected_call_states = (ccp.CallState.DIALING, ccp.CallState.ALERTING)
      expected_call_flag = ccp.CallFlag.IS_OUTGOING

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call state change",
    ):
      await ref_tbs_client.call_state.wait_for_target_value(
          lambda value: (
              len(value) >= 3
              and value[0] == 1
              and value[1] in expected_call_states
              and value[2] == expected_call_flag
          )
      )
      self.logger.info("[REF] Wait for call info change")
      await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(
          lambda value: (
              (info_list := ccp.CallInfo.parse_list(value))
              and (info_list[0].call_index == 1)
              and (info_list[0].call_state in expected_call_states)
              and (info_list[0].call_flags == expected_call_flag)
              and (info_list[0].call_uri == expected_call_uri)
          )
      )

    self.logger.info("[DUT] Answer / Activate call")
    call.answer()
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for call state to be active",
    ):
      await ref_tbs_client.call_state.wait_for_target_value(
          bytes([1, ccp.CallState.ACTIVE, expected_call_flag])
      )
      self.logger.info("[REF] Wait for call info change")
      await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(
          ccp.CallInfo(
              call_index=1,
              call_state=ccp.CallState.ACTIVE,
              call_flags=expected_call_flag,
              call_uri=expected_call_uri,
          ).to_bytes()
      )

  self.logger.info("[DUT] Terminate call")
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for call info removed",
  ):
    await ref_tbs_client.call_state.wait_for_target_value(b"")
    self.logger.info("[REF] Wait for call info change")
    await ref_tbs_client.bearer_list_current_calls.wait_for_target_value(b"")

Tests streaming with gaming context.

Test steps
  1. [Optional] Wait for audio streaming to stop if it is already streaming.
  2. Start audio streaming from DUT with gaming context and put a call on DUT.
  3. Wait for audio streaming to start from REF.
  4. Stop audio streaming from DUT and end the call.
  5. Wait for audio streaming to stop from REF.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
async def test_gaming_context(self) -> None:
  """Tests streaming with gaming context.

  Test steps:
    1. [Optional] Wait for audio streaming to stop if it is already streaming.
    2. Start audio streaming from DUT with gaming context and put a call on
    DUT.
    3. Wait for audio streaming to start from REF.
    4. Stop audio streaming from DUT and end the call.
    5. Wait for audio streaming to stop from REF.
  """
  sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]
  source_ase = self.ref_ascs.ase_state_machines[_SOURCE_ASE_ID]
  self.dut.bl4a.set_audio_attributes(
      bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.GAME),
      handle_audio_focus=False,
  )

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    for ase in self.ref_ascs.ase_state_machines.values():
      await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

  self.logger.info("[DUT] Put a VoIP call")
  call = self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.OUTGOING,
  )
  self.test_case_context.push(call)

  self.logger.info("[DUT] Start audio streaming")
  await asyncio.to_thread(self.dut.bt.audioPlaySine)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for sink ASE to start",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

  self.logger.info("[DUT] Start audio recording")
  recorder = await asyncio.to_thread(
      lambda: self.dut.bl4a.start_audio_recording(
          _RECORDING_PATH,
          source=bl4a_api.AudioRecorder.Source.VOICE_PERFORMANCE,
      )
  )
  self.test_case_context.push(recorder)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for source ASE to start",
  ):
    await _wait_for_ase_state(
        source_ase, ascs.AseStateMachine.State.STREAMING
    )

  # Check codec configuration.
  sink_freq = self._get_sampling_frequency(sink_ase)
  source_freq = self._get_sampling_frequency(source_ase)
  self.logger.info("sink_freq: %r, source_freq: %r", sink_freq, source_freq)

  if self.dut.getprop(_AndroidProperty.GMAP_ENABLED) == "true":
    # Asymmetric configuration is enabled with GMAP.
    expected_sink_freq = bap.SamplingFrequency.FREQ_48000
  else:
    expected_sink_freq = bap.SamplingFrequency.FREQ_32000
  self.assertEqual(sink_freq, expected_sink_freq)
  self.assertEqual(source_freq, bap.SamplingFrequency.FREQ_32000)

  # Streaming for 1 second.
  await asyncio.sleep(_STREAMING_TIME_SECONDS)

  self.logger.info("[DUT] Stop audio streaming")
  await asyncio.to_thread(self.dut.bt.audioStop)
  recorder.close()
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    for ase in self.ref_ascs.ase_state_machines.values():
      await _wait_for_ase_state(ase, ascs.AseStateMachine.State.IDLE)

Tests moving to previous and next track over MCP.

Test steps
  1. Connect MCP.
  2. Subscribe MCP characteristics.
  3. Fast forward over MCP.
  4. Fast rewind over MCP.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
async def test_mcp_fast_rewind_fast_forward(self) -> None:
  """Tests moving to previous and next track over MCP.

  Test steps:
    1. Connect MCP.
    2. Subscribe MCP characteristics.
    3. Fast forward over MCP.
    4. Fast rewind over MCP.
  """
  if not self.dut_mcp_enabled:
    self.skipTest("MCP is not enabled on DUT")

  self.logger.info("[REF] Connect MCP")
  ref_dut_acl = list(self.ref.device.connections.values())[0]
  async with device.Peer(ref_dut_acl) as peer:
    ref_mcp_client = peer.create_service_proxy(
        mcp.GenericMediaControlServiceProxy
    )
    if not ref_mcp_client:
      self.fail("Failed to connect MCP")

  # Push a long audio file to DUT.
  with tempfile.NamedTemporaryFile(
      # On Windows, NamedTemporaryFile cannot be deleted if used multiple
      # times.
      delete=(sys.platform != "win32")
  ) as local_file:
    with wave.open(local_file.name, "wb") as wave_file:
      wave_file.setnchannels(1)
      wave_file.setsampwidth(2)
      wave_file.setframerate(48000)
      wave_file.writeframes(bytes(48000 * 2 * 60))  # 60 seconds.
    self.dut.adb.push([
        local_file.name,
        f"/data/media/{self.dut.adb.current_user_id}/Music/sample.wav",
    ])

  await asyncio.to_thread(
      lambda: self.dut.bt.audioPlayFile(
          "/storage/self/primary/Music/sample.wav"
      )
  )

  watcher = pyee_extensions.EventWatcher()
  track_position = watcher.async_monitor(ref_mcp_client, "track_position")

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Subscribe MCP characteristics",
  ):
    await ref_mcp_client.subscribe_characteristics()

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Fast forward",
  ):
    self.assertEqual(
        await ref_mcp_client.write_control_point(_McpOpcode.FAST_FORWARD),
        mcp.MediaControlPointResultCode.SUCCESS,
    )
    self.logger.info("[REF] Wait for track position changed")
    await track_position.get()

  # Clear the track changed events.
  while not track_position.empty():
    track_position.get_nowait()

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Fast rewind",
  ):
    self.assertEqual(
        await ref_mcp_client.write_control_point(_McpOpcode.FAST_REWIND),
        mcp.MediaControlPointResultCode.SUCCESS,
    )
    self.logger.info("[REF] Wait for track position changed")
    await track_position.get()

Tests starting and stopping audio streaming over MCP.

Test steps
  1. Connect MCP.
  2. Subscribe MCP characteristics.
  3. Play audio streaming over MCP.
  4. Pause audio streaming over MCP.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
async def test_mcp_play_pause(self) -> None:
  """Tests starting and stopping audio streaming over MCP.

  Test steps:
    1. Connect MCP.
    2. Subscribe MCP characteristics.
    3. Play audio streaming over MCP.
    4. Pause audio streaming over MCP.
  """
  if not self.dut_mcp_enabled:
    self.skipTest("MCP is not enabled on DUT")

  self.logger.info("[REF] Connect MCP")
  ref_dut_acl = list(self.ref.device.connections.values())[0]
  async with device.Peer(ref_dut_acl) as peer:
    ref_mcp_client = peer.create_service_proxy(
        mcp.GenericMediaControlServiceProxy
    )
    if not ref_mcp_client:
      self.fail("Failed to connect MCP")

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Subscribe MCP characteristics",
  ):
    await ref_mcp_client.subscribe_characteristics()
  assert ref_mcp_client.media_state
  media_state = await gatt_helper.MutableCharacteristicState.create(
      ref_mcp_client.media_state
  )

  # Make sure player is active but not streaming.
  await asyncio.to_thread(self.dut.bt.audioPlaySine)
  await asyncio.to_thread(self.dut.bt.audioPause)
  await asyncio.sleep(_PREPARE_TIME_SECONDS)

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Play",
  ):
    self.assertEqual(
        await ref_mcp_client.write_control_point(_McpOpcode.PLAY),
        mcp.MediaControlPointResultCode.SUCCESS,
    )
    self.logger.info("[REF] Wait for media state to be PLAY")
    await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))

  # Streaming for 1 second.
  await asyncio.sleep(_STREAMING_TIME_SECONDS)

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Pause",
  ):
    self.assertEqual(
        await ref_mcp_client.write_control_point(_McpOpcode.PAUSE),
        mcp.MediaControlPointResultCode.SUCCESS,
    )
    self.logger.info("[REF] Wait for media state to be PAUSED")
    await media_state.wait_for_target_value(bytes([mcp.MediaState.PAUSED]))

Tests moving to previous and next track over MCP.

Test steps
  1. Connect MCP.
  2. Subscribe MCP characteristics.
  3. Move to next track over MCP.
  4. Move to previous track over MCP.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
async def test_mcp_previous_next_track(self) -> None:
  """Tests moving to previous and next track over MCP.

  Test steps:
    1. Connect MCP.
    2. Subscribe MCP characteristics.
    3. Move to next track over MCP.
    4. Move to previous track over MCP.
  """
  if not self.dut_mcp_enabled:
    self.skipTest("MCP is not enabled on DUT")

  self.logger.info("[REF] Connect MCP")
  ref_dut_acl = list(self.ref.device.connections.values())[0]
  async with device.Peer(ref_dut_acl) as peer:
    ref_mcp_client = peer.create_service_proxy(
        mcp.GenericMediaControlServiceProxy
    )
    if not ref_mcp_client:
      self.fail("Failed to connect MCP")

  # Allow repeating to avoid the end of the track.
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
  # Generate a sine wave audio file, and push it to DUT twice.
  with tempfile.NamedTemporaryFile(
      # On Windows, NamedTemporaryFile cannot be deleted if used multiple
      # times.
      delete=(sys.platform != "win32")
  ) as local_file:
    with wave.open(local_file.name, "wb") as wave_file:
      wave_file.setnchannels(1)
      wave_file.setsampwidth(2)
      wave_file.setframerate(48000)
      wave_file.writeframes(bytes(48000 * 2 * 5))  # 5 seconds.
    for i in range(2):
      self.dut.adb.push([
          local_file.name,
          f"/data/media/{self.dut.adb.current_user_id}/Music/sample-{i}.wav",
      ])

  dut_player_cb = self.dut.bl4a.register_callback(bl4a_api.Module.PLAYER)
  self.test_case_context.push(dut_player_cb)
  # Play the first track.
  self.dut.bt.audioPlayFile("/storage/self/primary/Music/sample-0.wav")
  # Add the second track to the player.
  self.dut.bt.addMediaItem("/storage/self/primary/Music/sample-1.wav")

  self.logger.info("[DUT] Wait for playback started.")
  await dut_player_cb.wait_for_event(
      bl4a_api.PlayerIsPlayingChanged(is_playing=True)
  )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Subscribe MCP characteristics",
  ):
    await ref_mcp_client.subscribe_characteristics()

  await asyncio.sleep(_PREPARE_TIME_SECONDS)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Move to next track",
  ):
    result = await ref_mcp_client.write_control_point(_McpOpcode.NEXT_TRACK)
    self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)

  self.logger.info("[DUT] Wait for playback changed.")
  await dut_player_cb.wait_for_event(
      bl4a_api.PlayerMediaItemTransition(
          uri="/storage/self/primary/Music/sample-1.wav"
      ),
  )

  await asyncio.sleep(_PREPARE_TIME_SECONDS)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Move to previous track",
  ):
    result = await ref_mcp_client.write_control_point(
        _McpOpcode.PREVIOUS_TRACK
    )
    self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)

  self.logger.info("[DUT] Wait for playback changed.")
  await dut_player_cb.wait_for_event(
      bl4a_api.PlayerMediaItemTransition(
          uri="/storage/self/primary/Music/sample-0.wav"
      ),
  )

Tests enabling noisy handling, and verify the player is paused after REF disconnected.

Test steps
  1. Enable noisy handling.
  2. Start streaming.
  3. Disconnect from REF.
  4. Wait for player paused.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
async def test_noisy_handling(self) -> None:
  """Tests enabling noisy handling, and verify the player is paused after REF disconnected.

  Test steps:
    1. Enable noisy handling.
    2. Start streaming.
    3. Disconnect from REF.
    4. Wait for player paused.
  """
  if self.dut.device.is_emulator:
    self.skipTest(
        "b/434613780 - Disconnection on streaming may cause Rootcanal crash."
    )

  # Enable audio noisy handling.
  self.dut.bt.setHandleAudioBecomingNoisy(True)

  sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for ASE state to be idle",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

  self.logger.info("[DUT] Start audio streaming")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)
  await asyncio.to_thread(self.dut.bt.audioPlaySine)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for ASE state to be streaming",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

  # Streaming for 1 second.
  await asyncio.sleep(_STREAMING_TIME_SECONDS)

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.PLAYER
  ) as dut_player_cb:
    ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address), transport=core.BT_LE_TRANSPORT
    )
    if ref_dut_acl is None:
      self.fail("No ACL connection found?")
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Disconnect.",
    ):
      await ref_dut_acl.disconnect()

    self.logger.info("[DUT] Wait for player paused.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=False),
    )

Tests reconfiguration from media to conversational.

Test steps
  1. [Optional] Wait for audio streaming to stop if it is already streaming.
  2. Start audio streaming from DUT.
  3. Wait for audio streaming to start from REF.
  4. Put a call on DUT to trigger reconfiguration.
  5. Wait for ASE to be reconfigured.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
async def test_reconfiguration(self) -> None:
  """Tests reconfiguration from media to conversational.

  Test steps:
    1. [Optional] Wait for audio streaming to stop if it is already streaming.
    2. Start audio streaming from DUT.
    3. Wait for audio streaming to start from REF.
    4. Put a call on DUT to trigger reconfiguration.
    5. Wait for ASE to be reconfigured.
  """
  sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

  self.logger.info("[DUT] Start audio streaming")
  await asyncio.to_thread(self.dut.bt.audioPlaySine)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to start",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)
  get_audio_context = lambda: next(
      entry
      for entry in sink_ase.metadata.entries
      if entry.tag == le_audio.Metadata.Tag.STREAMING_AUDIO_CONTEXTS
  )
  context_type = struct.unpack_from("<H", get_audio_context().data)[0]
  self.assertNotEqual(context_type, bap.ContextType.PROHIBITED)
  self.assertFalse(context_type & bap.ContextType.CONVERSATIONAL)

  # Streaming for 1 second.
  await asyncio.sleep(_STREAMING_TIME_SECONDS)

  call = self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.OUTGOING,
  )
  with call:
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[DUT] Wait for ASE to be released",
    ):
      await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[DUT] Wait for ASE to be reconfigured",
    ):
      await _wait_for_ase_state(
          sink_ase, ascs.AseStateMachine.State.STREAMING
      )
    context_type = struct.unpack_from("<H", get_audio_context().data)[0]
    self.assertTrue(context_type & bap.ContextType.CONVERSATIONAL)

Tests to reconnect the LE Audio Unicast server.

Parameters:

Name Type Description Default
is_active bool

True if reconnect is actively initialized by DUT, otherwise TA will be used to perform the reconnection passively.

required
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
@navi_test_base.named_parameterized(
    ("active", True),
    ("passive", False),
)
@navi_test_base.retry(_DEFAULT_RETRY_COUNT)
async def test_reconnect(self, is_active: bool) -> None:
  """Tests to reconnect the LE Audio Unicast server.

  Args:
    is_active: True if reconnect is actively initialized by DUT, otherwise TA
      will be used to perform the reconnection passively.
  """
  if not is_active and self.dut.device.is_emulator:
    self.skipTest(
        "b/425668688 - TA filter reconnection is not supported on rootcanal"
        " yet."
    )

  with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
    self.logger.info("[DUT] Disconnect REF")
    self.dut.bt.disconnect(self.ref.random_address)

    self.logger.info("[DUT] Wait for LE Audio disconnected")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=None),
    )

    self.logger.info("[REF] Start advertising")
    await self.ref.device.create_advertising_set(
        advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
        advertising_data=bytes(
            bap.UnicastServerAdvertisingData(
                announcement_type=bap.AnnouncementType.GENERAL
                if is_active
                else bap.AnnouncementType.TARGETED,
            )
        ),
    )
    if is_active:
      self.logger.info("[DUT] Reconnect REF")
      self.dut.bt.connect(self.ref.random_address)

    self.logger.info("[DUT] Wait for LE Audio connected")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
    )

Tests reconnecting during a call. Call audio should be routed to Unicast.

Test steps
  1. Disconnect REF.
  2. Put a call on DUT.
  3. Reconnect REF.
  4. Wait for audio streaming to start from REF.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
async def test_reconnect_during_call(self) -> None:
  """Tests reconnecting during a call. Call audio should be routed to Unicast.

  Test steps:
    1. Disconnect REF.
    2. Put a call on DUT.
    3. Reconnect REF.
    4. Wait for audio streaming to start from REF.
  """
  if self.dut.device.is_emulator:
    self.skipTest(
        "b/425668688 - TA filter reconnection is not supported on rootcanal"
        " yet."
    )

  with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
    self.logger.info("[DUT] Disconnect REF")
    self.dut.bt.disconnect(self.ref.random_address)

    self.logger.info("[DUT] Wait for LE Audio disconnected")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=None),
    )

  with contextlib.ExitStack() as stack:
    dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
    stack.enter_context(dut_telecom_cb)
    self.logger.info("[DUT] Put a call")
    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    )
    stack.enter_context(call)
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged,
        lambda e: (e.state in (_CallState.CONNECTING, _CallState.DIALING)),
    )
    # Start audio streaming from DUT.
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    self.dut.bt.audioPlaySine()
    recorder = await asyncio.to_thread(
        lambda: self.dut.bl4a.start_audio_recording(
            _RECORDING_PATH,
            source=bl4a_api.AudioRecorder.Source.VOICE_COMMUNICATION,
        )
    )
    stack.enter_context(recorder)

    dut_leaudio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO)
    stack.enter_context(dut_leaudio_cb)

    self.logger.info("[REF] Start advertising")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await self.ref.device.create_advertising_set(
          advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
          advertising_data=bytes(
              bap.UnicastServerAdvertisingData(
                  announcement_type=bap.AnnouncementType.TARGETED
              )
          ),
      )
    self.logger.info("[DUT] Wait for LE Audio connected")
    await dut_leaudio_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
    )

    self.logger.info("[REF] Wait for streaming to start")
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Wait for audio to start",
    ):
      for ase in self.ref_ascs.ase_state_machines.values():
        await _wait_for_ase_state(ase, ascs.AseStateMachine.State.STREAMING)

Tests setting volume over LEA VCP from DUT or REF.

Test steps
  1. Set volume from DUT or REF.
  2. Wait for the volume to be set correctly on the other device.

Parameters:

Name Type Description Default
issuer _TestRole

The issuer of the volume setting.

required
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
@navi_test_base.parameterized(_TestRole.DUT, _TestRole.REF)
async def test_set_volume(self, issuer: _TestRole) -> None:
  """Tests setting volume over LEA VCP from DUT or REF.

  Test steps:
    1. Set volume from DUT or REF.
    2. Wait for the volume to be set correctly on the other device.

  Args:
    issuer: The issuer of the volume setting.
  """
  if not self.dut_vcp_enabled:
    self.skipTest("VCP is not enabled on DUT")

  dut_max_volume = self.dut.bt.getMaxVolume(_StreamType.MUSIC)
  dut_expected_volume = (self.dut.bt.getVolume(_StreamType.MUSIC) + 1) % (
      dut_max_volume + 1
  )
  ref_expected_volume = int(
      decimal.Decimal(
          dut_expected_volume / dut_max_volume * vcs.MAX_VOLUME
      ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
  )

  # DUT's VCS client might not be stable at the beginning. If we set volume
  # immediately, the volume might not be set correctly.
  await asyncio.sleep(_PREPARE_TIME_SECONDS)

  with (
      self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO) as dut_audio_cb,
  ):
    vcs_volume = pyee_extensions.EventTriggeredValueObserver[int](
        self.ref_vcs,
        self.ref_vcs.EVENT_VOLUME_STATE_CHANGE,
        lambda: self.ref_vcs.volume_setting,
    )
    if issuer == _TestRole.DUT:
      self.logger.info("[DUT] Set volume to %d", dut_expected_volume)
      self.dut.bt.setVolume(_StreamType.MUSIC, dut_expected_volume)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Wait for volume to be set",
      ):
        await vcs_volume.wait_for_target_value(ref_expected_volume)
    else:
      self.logger.info("[REF] Set volume to %d", ref_expected_volume)
      self.ref_vcs.volume_setting = ref_expected_volume
      await self.ref.device.notify_subscribers(self.ref_vcs.volume_state)
      await dut_audio_cb.wait_for_event(
          event=bl4a_api.VolumeChanged(
              stream_type=_StreamType.MUSIC,
              volume_value=int(dut_expected_volume),
          ),
      )

Tests unidirectional audio stream between DUT and REF.

Test steps
  1. [Optional] Wait for audio streaming to stop if it is already streaming.
  2. Start audio streaming from DUT.
  3. Wait for audio streaming to start from REF.
  4. Stop audio streaming from DUT.
  5. Wait for audio streaming to stop from REF.
Source code in navi/tests/smoke/le_audio_unicast_client_test.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
async def test_unidirectional_audio_stream(self) -> None:
  """Tests unidirectional audio stream between DUT and REF.

  Test steps:
    1. [Optional] Wait for audio streaming to stop if it is already streaming.
    2. Start audio streaming from DUT.
    3. Wait for audio streaming to start from REF.
    4. Stop audio streaming from DUT.
    5. Wait for audio streaming to stop from REF.
  """
  sink_ase = self.ref_ascs.ase_state_machines[_SINK_ASE_ID]

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

  self.logger.info("[DUT] Start audio streaming")
  await asyncio.to_thread(self.dut.bt.audioPlaySine)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to start",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.STREAMING)

  # Setup audio sink.
  sink_frames = list[bytes]()
  decoder = decoder_for_ase(sink_ase) if lc3 else None

  def sink(pdu: hci.HCI_IsoDataPacket):
    if pdu.iso_sdu_fragment:
      sink_frames.append(pdu.iso_sdu_fragment)

  assert (cis_link := sink_ase.cis_link)
  cis_link.sink = sink

  # Streaming for 1 second.
  await asyncio.sleep(_STREAMING_TIME_SECONDS)

  self.logger.info("[DUT] Stop audio streaming")
  cis_link.sink = None
  await asyncio.to_thread(self.dut.bt.audioStop)
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF] Wait for audio to stop",
  ):
    await _wait_for_ase_state(sink_ase, ascs.AseStateMachine.State.IDLE)

  if self.user_params.get(navi_test_base.RECORD_FULL_DATA):
    self.write_test_output_data("sink.lc3", b"".join(sink_frames))
  if lc3 and decoder and audio.SUPPORT_AUDIO_PROCESSING:
    pcm_format = lc3.PcmFormat.SIGNED_16
    decoded_frames = [
        decoder.decode(frame, pcm_format) for frame in sink_frames
    ]
    dominant_frequency = audio.get_dominant_frequency(
        buffer=b"".join(decoded_frames),
        format="pcm",
        sample_width=pcm_format.sample_width,
        frame_rate=_DEFAULT_FRAME_RATE,
        channels=decoder.num_channels,
    )
    self.logger.info("dominant_frequency: %.2f", dominant_frequency)
    self.assertAlmostEqual(dominant_frequency, 1000, delta=10)

Makes sure DUT sets the volume correctly after connecting to REF.

Source code in navi/tests/smoke/le_audio_unicast_client_test.py
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
async def test_volume_initialization(self) -> None:
  """Makes sure DUT sets the volume correctly after connecting to REF."""
  if not self.dut_vcp_enabled:
    self.skipTest("VCP is not enabled on DUT")

  # When the flag is enabled, DUT's volume will be applied to REF.
  if self.dut.bluetooth_flags.get("vcp_device_volume_api_improvements", True):
    vcs_volume = pyee_extensions.EventTriggeredValueObserver[int](
        self.ref_vcs,
        self.ref_vcs.EVENT_VOLUME_STATE_CHANGE,
        lambda: self.ref_vcs.volume_setting,
    )
    ref_expected_volume = decimal.Decimal(
        self.dut.bt.getVolume(_StreamType.MUSIC)
        / self.dut.bt.getMaxVolume(_StreamType.MUSIC)
        * vcs.MAX_VOLUME
    ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        "[REF] Wait for volume to be synced with DUT",
    ):
      await vcs_volume.wait_for_target_value(int(ref_expected_volume))
  else:
    dut_expected_volume = decimal.Decimal(
        self.ref_vcs.volume_setting
        / vcs.MAX_VOLUME
        * self.dut.bt.getMaxVolume(_StreamType.MUSIC)
    ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
    with (
        self.dut.bl4a.register_callback(
            bl4a_api.Module.AUDIO
        ) as dut_audio_cb,
    ):
      if self.dut.bt.getVolume(_StreamType.MUSIC) != dut_expected_volume:
        self.logger.info("[DUT] Wait for volume to be synced with REF")
        await dut_audio_cb.wait_for_event(
            event=bl4a_api.VolumeChanged(
                stream_type=_StreamType.MUSIC,
                volume_value=int(dut_expected_volume),
            ),
        )

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/le_host_test.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
class LeHostTest(navi_test_base.TwoDevicesTestBase):

  @navi_test_base.parameterized(
      _OwnAddressType.PUBLIC,
      _OwnAddressType.RANDOM,
  )
  @navi_test_base.retry(max_count=2)
  async def test_outgoing_connect_disconnect(
      self, ref_address_type: hci.OwnAddressType
  ) -> None:
    """Tests outgoing LE connection and disconnection.

    Test steps:
      1. Start advertising on REF.
      2. Connect REF from DUT.
      3. Wait for BLE connected.
      4. Disconnect REF from DUT.

    Args:
      ref_address_type: address type of REF device used in advertisements.
    """
    match ref_address_type:
      case _OwnAddressType.PUBLIC:
        ref_address = str(self.ref.address)
      case _OwnAddressType.RANDOM:
        ref_address = str(self.ref.random_address)
      case _:
        self.fail(f"Invalid address type {ref_address_type}.")

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:

      # [REF] Start advertising.
      await self.ref.device.start_advertising(
          own_address_type=ref_address_type,
          advertising_type=device.AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
          advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
          advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
      )

      # [DUT] Connect GATT.
      gatt_client = await self.dut.bl4a.connect_gatt_client(
          address=ref_address,
          transport=android_constants.Transport.LE,
          address_type=ref_address_type,
      )
      await dut_cb.wait_for_event(
          event=bl4a_api.AclConnected(
              address=ref_address, transport=android_constants.Transport.LE
          ),
      )
      # [DUT] Disconnect GATT.
      await gatt_client.disconnect()
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=ref_address,
              transport=android_constants.Transport.LE,
          ),
      )

  @navi_test_base.retry(max_count=2)
  async def test_incoming_connect_disconnect(self) -> None:
    """Tests incoming LE connection and disconnection.

    Test steps:
      1. Start advertising on DUT.
      2. Connect DUT from REF.
      3. Wait for BLE connected.
      4. Disconnect DUT from REF.
    """

    # [DUT] Start advertising with Public address.
    await self.dut.bl4a.start_legacy_advertiser(
        bl4a_api.LegacyAdvertiseSettings(
            own_address_type=_OwnAddressType.PUBLIC
        ),
    )

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      # [REF] Connect GATT.
      ref_dut_acl = await self.ref.device.connect(
          f"{self.dut.address}/P",
          core.BT_LE_TRANSPORT,
          own_address_type=_OwnAddressType.PUBLIC,
      )
      await ref_dut_acl.get_remote_le_features()

      # [DUT] Wait for LE-ACL connected.
      await dut_cb.wait_for_event(
          event=bl4a_api.AclConnected(
              address=self.ref.address, transport=android_constants.Transport.LE
          ),
      )

      # [REF] Disconnect.
      await ref_dut_acl.disconnect()
      # [DUT] Wait for LE-ACL disconnected.
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.LE,
          ),
      )

  @navi_test_base.parameterized(
      _AdvertisingVariant.LEGACY_NO_ADV_DATA,
      _AdvertisingVariant.EXTENDED_ADV_DATA_1_BYTES,
      _AdvertisingVariant.EXTENDED_ADV_DATA_200_BYTES,
  )
  async def test_scan(
      self, ref_advertising_variant: _AdvertisingVariant
  ) -> None:
    """Tests scanning remote devices.

    Test steps:
      1. Start advertising on REF.
      2. Start scanning on DUT.
      3. Wait for matched scan result.

    Args:
      ref_advertising_variant: advertising variant of REF device.
    """
    match ref_advertising_variant:
      case _AdvertisingVariant.LEGACY_NO_ADV_DATA:
        advertising_data = b""
        advertising_properties = device.AdvertisingEventProperties(
            is_connectable=True,
            is_scannable=True,
            is_legacy=True,
        )
      case _AdvertisingVariant.EXTENDED_ADV_DATA_1_BYTES:
        advertising_data = bytes(1)
        advertising_properties = device.AdvertisingEventProperties(
            is_connectable=True,
        )
      case _AdvertisingVariant.EXTENDED_ADV_DATA_200_BYTES:
        advertising_data = bytes(200)
        advertising_properties = device.AdvertisingEventProperties(
            is_connectable=True,
        )
      case _:
        self.fail(f"Invalid advertising variant {ref_advertising_variant}.")

    # [REF] Start advertising.
    await self.ref.device.create_advertising_set(
        advertising_parameters=device.AdvertisingParameters(
            primary_advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
            primary_advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
            own_address_type=_OwnAddressType.PUBLIC,
            advertising_event_properties=advertising_properties,
        ),
        advertising_data=advertising_data,
    )
    # [DUT] Start scanning.
    with self.dut.bl4a.start_scanning(
        scan_settings=bl4a_api.ScanSettings(
            legacy=False,
        ),
        scan_filter=bl4a_api.ScanFilter(
            device=self.ref.address,
            address_type=android_constants.AddressTypeStatus.PUBLIC,
        ),
    ) as scan_cb:
      # [DUT] Wait for advertising report(scan result) from REF.
      event = await scan_cb.wait_for_event(bl4a_api.ScanResult)
      self.assertEqual(event.address, self.ref.address)

  async def test_advertising_with_service_uuid(self) -> None:
    """Tests advertising using RPA, with Service UUID included in AdvertisingData.

    Test steps:
      1. Start advertising on DUT.
      2. Start scanning on REF.
      3. Wait for matched scan result.
    """
    with pyee_extensions.EventWatcher() as watcher:
      # Generate a random UUID for testing.
      service_uuid = str(uuid.uuid4())

      # [DUT] Start advertising with service UUID and RPA.
      advertise = await self.dut.bl4a.start_legacy_advertiser(
          bl4a_api.LegacyAdvertiseSettings(
              own_address_type=_OwnAddressType.PUBLIC
          ),
          bl4a_api.AdvertisingData(service_uuids=[service_uuid]),
      )

      # [REF] Scan for DUT.
      scan_results = asyncio.Queue[device.Advertisement]()

      @watcher.on(self.ref.device, self.ref.device.EVENT_ADVERTISEMENT)
      def _(adv: device.Advertisement) -> None:
        if (
            service_uuids := adv.data.get(
                _AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
            )
        ) and service_uuid in service_uuids:
          scan_results.put_nowait(adv)

      await self.ref.device.start_scanning()
      # [REF] Wait for advertising report(scan result) from DUT.
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        await scan_results.get()
      advertise.stop()

  async def test_advertising_with_public_address(self) -> None:
    """Tests advertising using Public Address.

    Test steps:
      1. Start advertising on DUT.
      2. Start scanning on REF.
      3. Wait for matched scan result.
    """
    with pyee_extensions.EventWatcher() as watcher:
      # [DUT] Start advertising with service UUID and Public address.
      advertise = await self.dut.bl4a.start_legacy_advertiser(
          bl4a_api.LegacyAdvertiseSettings(
              own_address_type=_OwnAddressType.PUBLIC
          ),
      )

      # [REF] Scan for DUT.
      scan_results = asyncio.Queue[device.Advertisement]()
      dut_address = hci.Address(f"{self.dut.address}/P")

      @watcher.on(self.ref.device, self.ref.device.EVENT_ADVERTISEMENT)
      def on_advertising_report(adv: device.Advertisement) -> None:
        if adv.address == dut_address:
          scan_results.put_nowait(adv)

      await self.ref.device.start_scanning()
      # [REF] Wait for advertising report(scan result) from DUT.
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        await scan_results.get()
      advertise.stop()

  @navi_test_base.parameterized(
      *itertools.product(
          (hci.Phy.LE_1M, hci.Phy.LE_2M, hci.Phy.LE_CODED),
          (
              android_constants.AddressTypeStatus.PUBLIC,
              android_constants.AddressTypeStatus.RANDOM,
              android_constants.AddressTypeStatus.RANDOM_NON_RESOLVABLE,
          ),
      )
  )
  async def test_extended_advertising(
      self, phy: int, own_address_type: android_constants.AddressTypeStatus
  ) -> None:
    """Tests extended advertising, with different primary Phy settings.

    Test steps:
      1. Start advertising on DUT.
      2. Start scanning on REF.
      3. Wait for matched scan result.

    Args:
      phy: PHY option used in extended advertising.
      own_address_type: type of address used in the advertisement.
    """
    # Generate a random UUID for testing.
    service_uuid = str(uuid.uuid4())

    self.logger.info("[DUT] Start advertising with service UUID.")
    advertise = await self.dut.bl4a.start_extended_advertising_set(
        bl4a_api.AdvertisingSetParameters(
            secondary_phy=phy,
            own_address_type=own_address_type,
        ),
        bl4a_api.AdvertisingData(service_uuids=[service_uuid]),
    )

    # [REF] Scan for DUT.
    scan_results = asyncio.Queue[device.Advertisement]()

    def on_advertising_report(adv: device.Advertisement) -> None:
      if (
          service_uuids := adv.data.get(
              _AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
          )
      ) and service_uuid in service_uuids:
        scan_results.put_nowait(adv)

    with pyee_extensions.EventWatcher() as watcher:
      watcher.on(self.ref.device, "advertisement", on_advertising_report)

      self.logger.info("[REF] Start scanning for DUT.")
      await self.ref.device.start_scanning()

      self.logger.info("[REF] Wait for advertising report from DUT.")
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        advertisement = await scan_results.get()
      advertise.stop()
      self.assertEqual(advertisement.secondary_phy, phy)

      match own_address_type:
        case android_constants.AddressTypeStatus.PUBLIC:
          self.assertEqual(
              advertisement.address, hci.Address(f"{self.dut.address}/P")
          )
        case android_constants.AddressTypeStatus.RANDOM:
          self.assertTrue(advertisement.address.is_random)
          self.assertTrue(advertisement.address.is_resolvable)
        case android_constants.AddressTypeStatus.RANDOM_NON_RESOLVABLE:
          self.assertTrue(advertisement.address.is_random)
          self.assertFalse(advertisement.address.is_resolvable)
        case _:
          self.fail(f"Invalid address type {own_address_type}.")

  @navi_test_base.retry(max_count=2)
  async def test_le_discovery(self) -> None:
    """Test discover LE devices.

    Test steps:
      1. Disable Classic scan and start advertising on REF.
      2. Start discovery on REF.
      3. Wait for matched scan result.
    """
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:

      await self.ref.device.set_scan_enable(
          inquiry_scan_enabled=0, page_scan_enabled=0
      )
      # [REF] Start advertising.
      await self.ref.device.start_advertising(
          own_address_type=_OwnAddressType.PUBLIC,
          advertising_type=device.AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
          advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
          advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
          advertising_data=bytes(
              _AdvertisingData([
                  (
                      _AdvertisingData.FLAGS,
                      bytes(
                          [_AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]
                      ),
                  ),
                  (
                      _AdvertisingData.COMPLETE_LOCAL_NAME,
                      "Super Bumble".encode(),
                  ),
              ])
          ),
      )
      self.dut.bt.startInquiry()

      await dut_cb.wait_for_event(
          bl4a_api.DeviceFound,
          lambda e: (e.address == self.ref.address),
          _DISCOVERY_TIMEOUT_SECONDS,
      )

  @navi_test_base.parameterized(
      hci.OwnAddressType.PUBLIC,
      hci.OwnAddressType.RANDOM,
      hci.OwnAddressType.RESOLVABLE_OR_RANDOM,
      hci.OwnAddressType.RESOLVABLE_OR_PUBLIC,
  )
  async def test_scan_and_connect_after_pairing(
      self, ref_address_type: hci.OwnAddressType
  ) -> None:
    """Tests scanning remote devices after pairing(IRK exchanged).

    Test steps:
      1. Pair with REF.
      2. Disconnect from REF.
      3. Start advertising on REF.
      4. Start scanning on DUT.
      5. Wait for matched scan result.

    Args:
      ref_address_type: address type of REF device used in advertisements.
    """
    if ref_address_type in (
        hci.OwnAddressType.RESOLVABLE_OR_RANDOM,
        hci.OwnAddressType.RANDOM,
    ):
      identity_address = self.ref.random_address
      identity_address_type = android_constants.AddressTypeStatus.RANDOM
    else:
      identity_address = self.ref.address
      identity_address_type = android_constants.AddressTypeStatus.PUBLIC

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      self.logger.info("[DUT] Pair with REF.")
      await self.le_connect_and_pair(identity_address_type)

      if ref_dut_acl := self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address, hci.AddressType.PUBLIC_DEVICE),
          core.BT_LE_TRANSPORT,
      ):
        self.logger.info("[REF] Disconnect.")
        with contextlib.suppress(hci.HCI_StatusError):
          await ref_dut_acl.disconnect()
      await dut_cb.wait_for_event(bl4a_api.AclDisconnected)

    self.logger.info("[REF] Start advertising.")
    await self.ref.device.start_advertising(own_address_type=ref_address_type)

    self.logger.info("[DUT] Start scanning for REF.")
    dut_scanner = self.dut.bl4a.start_scanning(
        scan_filter=bl4a_api.ScanFilter(
            device=identity_address,
            address_type=identity_address_type,
        ),
    )
    await dut_scanner.wait_for_event(bl4a_api.ScanResult)
    self.logger.info("[DUT] Found REF, start connecting GATT.")
    await self.dut.bl4a.connect_gatt_client(
        address=identity_address,
        address_type=identity_address_type,
        transport=android_constants.Transport.LE,
    )

Tests advertising using Public Address.

Test steps
  1. Start advertising on DUT.
  2. Start scanning on REF.
  3. Wait for matched scan result.
Source code in navi/tests/smoke/le_host_test.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
async def test_advertising_with_public_address(self) -> None:
  """Tests advertising using Public Address.

  Test steps:
    1. Start advertising on DUT.
    2. Start scanning on REF.
    3. Wait for matched scan result.
  """
  with pyee_extensions.EventWatcher() as watcher:
    # [DUT] Start advertising with service UUID and Public address.
    advertise = await self.dut.bl4a.start_legacy_advertiser(
        bl4a_api.LegacyAdvertiseSettings(
            own_address_type=_OwnAddressType.PUBLIC
        ),
    )

    # [REF] Scan for DUT.
    scan_results = asyncio.Queue[device.Advertisement]()
    dut_address = hci.Address(f"{self.dut.address}/P")

    @watcher.on(self.ref.device, self.ref.device.EVENT_ADVERTISEMENT)
    def on_advertising_report(adv: device.Advertisement) -> None:
      if adv.address == dut_address:
        scan_results.put_nowait(adv)

    await self.ref.device.start_scanning()
    # [REF] Wait for advertising report(scan result) from DUT.
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await scan_results.get()
    advertise.stop()

Tests advertising using RPA, with Service UUID included in AdvertisingData.

Test steps
  1. Start advertising on DUT.
  2. Start scanning on REF.
  3. Wait for matched scan result.
Source code in navi/tests/smoke/le_host_test.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
async def test_advertising_with_service_uuid(self) -> None:
  """Tests advertising using RPA, with Service UUID included in AdvertisingData.

  Test steps:
    1. Start advertising on DUT.
    2. Start scanning on REF.
    3. Wait for matched scan result.
  """
  with pyee_extensions.EventWatcher() as watcher:
    # Generate a random UUID for testing.
    service_uuid = str(uuid.uuid4())

    # [DUT] Start advertising with service UUID and RPA.
    advertise = await self.dut.bl4a.start_legacy_advertiser(
        bl4a_api.LegacyAdvertiseSettings(
            own_address_type=_OwnAddressType.PUBLIC
        ),
        bl4a_api.AdvertisingData(service_uuids=[service_uuid]),
    )

    # [REF] Scan for DUT.
    scan_results = asyncio.Queue[device.Advertisement]()

    @watcher.on(self.ref.device, self.ref.device.EVENT_ADVERTISEMENT)
    def _(adv: device.Advertisement) -> None:
      if (
          service_uuids := adv.data.get(
              _AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
          )
      ) and service_uuid in service_uuids:
        scan_results.put_nowait(adv)

    await self.ref.device.start_scanning()
    # [REF] Wait for advertising report(scan result) from DUT.
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await scan_results.get()
    advertise.stop()

Tests extended advertising, with different primary Phy settings.

Test steps
  1. Start advertising on DUT.
  2. Start scanning on REF.
  3. Wait for matched scan result.

Parameters:

Name Type Description Default
phy int

PHY option used in extended advertising.

required
own_address_type AddressTypeStatus

type of address used in the advertisement.

required
Source code in navi/tests/smoke/le_host_test.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
@navi_test_base.parameterized(
    *itertools.product(
        (hci.Phy.LE_1M, hci.Phy.LE_2M, hci.Phy.LE_CODED),
        (
            android_constants.AddressTypeStatus.PUBLIC,
            android_constants.AddressTypeStatus.RANDOM,
            android_constants.AddressTypeStatus.RANDOM_NON_RESOLVABLE,
        ),
    )
)
async def test_extended_advertising(
    self, phy: int, own_address_type: android_constants.AddressTypeStatus
) -> None:
  """Tests extended advertising, with different primary Phy settings.

  Test steps:
    1. Start advertising on DUT.
    2. Start scanning on REF.
    3. Wait for matched scan result.

  Args:
    phy: PHY option used in extended advertising.
    own_address_type: type of address used in the advertisement.
  """
  # Generate a random UUID for testing.
  service_uuid = str(uuid.uuid4())

  self.logger.info("[DUT] Start advertising with service UUID.")
  advertise = await self.dut.bl4a.start_extended_advertising_set(
      bl4a_api.AdvertisingSetParameters(
          secondary_phy=phy,
          own_address_type=own_address_type,
      ),
      bl4a_api.AdvertisingData(service_uuids=[service_uuid]),
  )

  # [REF] Scan for DUT.
  scan_results = asyncio.Queue[device.Advertisement]()

  def on_advertising_report(adv: device.Advertisement) -> None:
    if (
        service_uuids := adv.data.get(
            _AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
        )
    ) and service_uuid in service_uuids:
      scan_results.put_nowait(adv)

  with pyee_extensions.EventWatcher() as watcher:
    watcher.on(self.ref.device, "advertisement", on_advertising_report)

    self.logger.info("[REF] Start scanning for DUT.")
    await self.ref.device.start_scanning()

    self.logger.info("[REF] Wait for advertising report from DUT.")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      advertisement = await scan_results.get()
    advertise.stop()
    self.assertEqual(advertisement.secondary_phy, phy)

    match own_address_type:
      case android_constants.AddressTypeStatus.PUBLIC:
        self.assertEqual(
            advertisement.address, hci.Address(f"{self.dut.address}/P")
        )
      case android_constants.AddressTypeStatus.RANDOM:
        self.assertTrue(advertisement.address.is_random)
        self.assertTrue(advertisement.address.is_resolvable)
      case android_constants.AddressTypeStatus.RANDOM_NON_RESOLVABLE:
        self.assertTrue(advertisement.address.is_random)
        self.assertFalse(advertisement.address.is_resolvable)
      case _:
        self.fail(f"Invalid address type {own_address_type}.")

Tests incoming LE connection and disconnection.

Test steps
  1. Start advertising on DUT.
  2. Connect DUT from REF.
  3. Wait for BLE connected.
  4. Disconnect DUT from REF.
Source code in navi/tests/smoke/le_host_test.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
@navi_test_base.retry(max_count=2)
async def test_incoming_connect_disconnect(self) -> None:
  """Tests incoming LE connection and disconnection.

  Test steps:
    1. Start advertising on DUT.
    2. Connect DUT from REF.
    3. Wait for BLE connected.
    4. Disconnect DUT from REF.
  """

  # [DUT] Start advertising with Public address.
  await self.dut.bl4a.start_legacy_advertiser(
      bl4a_api.LegacyAdvertiseSettings(
          own_address_type=_OwnAddressType.PUBLIC
      ),
  )

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    # [REF] Connect GATT.
    ref_dut_acl = await self.ref.device.connect(
        f"{self.dut.address}/P",
        core.BT_LE_TRANSPORT,
        own_address_type=_OwnAddressType.PUBLIC,
    )
    await ref_dut_acl.get_remote_le_features()

    # [DUT] Wait for LE-ACL connected.
    await dut_cb.wait_for_event(
        event=bl4a_api.AclConnected(
            address=self.ref.address, transport=android_constants.Transport.LE
        ),
    )

    # [REF] Disconnect.
    await ref_dut_acl.disconnect()
    # [DUT] Wait for LE-ACL disconnected.
    await dut_cb.wait_for_event(
        bl4a_api.AclDisconnected(
            address=self.ref.address,
            transport=android_constants.Transport.LE,
        ),
    )

Test discover LE devices.

Test steps
  1. Disable Classic scan and start advertising on REF.
  2. Start discovery on REF.
  3. Wait for matched scan result.
Source code in navi/tests/smoke/le_host_test.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
@navi_test_base.retry(max_count=2)
async def test_le_discovery(self) -> None:
  """Test discover LE devices.

  Test steps:
    1. Disable Classic scan and start advertising on REF.
    2. Start discovery on REF.
    3. Wait for matched scan result.
  """
  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:

    await self.ref.device.set_scan_enable(
        inquiry_scan_enabled=0, page_scan_enabled=0
    )
    # [REF] Start advertising.
    await self.ref.device.start_advertising(
        own_address_type=_OwnAddressType.PUBLIC,
        advertising_type=device.AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
        advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
        advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
        advertising_data=bytes(
            _AdvertisingData([
                (
                    _AdvertisingData.FLAGS,
                    bytes(
                        [_AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG]
                    ),
                ),
                (
                    _AdvertisingData.COMPLETE_LOCAL_NAME,
                    "Super Bumble".encode(),
                ),
            ])
        ),
    )
    self.dut.bt.startInquiry()

    await dut_cb.wait_for_event(
        bl4a_api.DeviceFound,
        lambda e: (e.address == self.ref.address),
        _DISCOVERY_TIMEOUT_SECONDS,
    )

Tests outgoing LE connection and disconnection.

Test steps
  1. Start advertising on REF.
  2. Connect REF from DUT.
  3. Wait for BLE connected.
  4. Disconnect REF from DUT.

Parameters:

Name Type Description Default
ref_address_type OwnAddressType

address type of REF device used in advertisements.

required
Source code in navi/tests/smoke/le_host_test.py
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@navi_test_base.parameterized(
    _OwnAddressType.PUBLIC,
    _OwnAddressType.RANDOM,
)
@navi_test_base.retry(max_count=2)
async def test_outgoing_connect_disconnect(
    self, ref_address_type: hci.OwnAddressType
) -> None:
  """Tests outgoing LE connection and disconnection.

  Test steps:
    1. Start advertising on REF.
    2. Connect REF from DUT.
    3. Wait for BLE connected.
    4. Disconnect REF from DUT.

  Args:
    ref_address_type: address type of REF device used in advertisements.
  """
  match ref_address_type:
    case _OwnAddressType.PUBLIC:
      ref_address = str(self.ref.address)
    case _OwnAddressType.RANDOM:
      ref_address = str(self.ref.random_address)
    case _:
      self.fail(f"Invalid address type {ref_address_type}.")

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:

    # [REF] Start advertising.
    await self.ref.device.start_advertising(
        own_address_type=ref_address_type,
        advertising_type=device.AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
        advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
        advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
    )

    # [DUT] Connect GATT.
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        address=ref_address,
        transport=android_constants.Transport.LE,
        address_type=ref_address_type,
    )
    await dut_cb.wait_for_event(
        event=bl4a_api.AclConnected(
            address=ref_address, transport=android_constants.Transport.LE
        ),
    )
    # [DUT] Disconnect GATT.
    await gatt_client.disconnect()
    await dut_cb.wait_for_event(
        bl4a_api.AclDisconnected(
            address=ref_address,
            transport=android_constants.Transport.LE,
        ),
    )

Tests scanning remote devices.

Test steps
  1. Start advertising on REF.
  2. Start scanning on DUT.
  3. Wait for matched scan result.

Parameters:

Name Type Description Default
ref_advertising_variant _AdvertisingVariant

advertising variant of REF device.

required
Source code in navi/tests/smoke/le_host_test.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
@navi_test_base.parameterized(
    _AdvertisingVariant.LEGACY_NO_ADV_DATA,
    _AdvertisingVariant.EXTENDED_ADV_DATA_1_BYTES,
    _AdvertisingVariant.EXTENDED_ADV_DATA_200_BYTES,
)
async def test_scan(
    self, ref_advertising_variant: _AdvertisingVariant
) -> None:
  """Tests scanning remote devices.

  Test steps:
    1. Start advertising on REF.
    2. Start scanning on DUT.
    3. Wait for matched scan result.

  Args:
    ref_advertising_variant: advertising variant of REF device.
  """
  match ref_advertising_variant:
    case _AdvertisingVariant.LEGACY_NO_ADV_DATA:
      advertising_data = b""
      advertising_properties = device.AdvertisingEventProperties(
          is_connectable=True,
          is_scannable=True,
          is_legacy=True,
      )
    case _AdvertisingVariant.EXTENDED_ADV_DATA_1_BYTES:
      advertising_data = bytes(1)
      advertising_properties = device.AdvertisingEventProperties(
          is_connectable=True,
      )
    case _AdvertisingVariant.EXTENDED_ADV_DATA_200_BYTES:
      advertising_data = bytes(200)
      advertising_properties = device.AdvertisingEventProperties(
          is_connectable=True,
      )
    case _:
      self.fail(f"Invalid advertising variant {ref_advertising_variant}.")

  # [REF] Start advertising.
  await self.ref.device.create_advertising_set(
      advertising_parameters=device.AdvertisingParameters(
          primary_advertising_interval_min=_MIN_ADVERTISING_INTERVAL_MS,
          primary_advertising_interval_max=_MIN_ADVERTISING_INTERVAL_MS,
          own_address_type=_OwnAddressType.PUBLIC,
          advertising_event_properties=advertising_properties,
      ),
      advertising_data=advertising_data,
  )
  # [DUT] Start scanning.
  with self.dut.bl4a.start_scanning(
      scan_settings=bl4a_api.ScanSettings(
          legacy=False,
      ),
      scan_filter=bl4a_api.ScanFilter(
          device=self.ref.address,
          address_type=android_constants.AddressTypeStatus.PUBLIC,
      ),
  ) as scan_cb:
    # [DUT] Wait for advertising report(scan result) from REF.
    event = await scan_cb.wait_for_event(bl4a_api.ScanResult)
    self.assertEqual(event.address, self.ref.address)

Tests scanning remote devices after pairing(IRK exchanged).

Test steps
  1. Pair with REF.
  2. Disconnect from REF.
  3. Start advertising on REF.
  4. Start scanning on DUT.
  5. Wait for matched scan result.

Parameters:

Name Type Description Default
ref_address_type OwnAddressType

address type of REF device used in advertisements.

required
Source code in navi/tests/smoke/le_host_test.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
@navi_test_base.parameterized(
    hci.OwnAddressType.PUBLIC,
    hci.OwnAddressType.RANDOM,
    hci.OwnAddressType.RESOLVABLE_OR_RANDOM,
    hci.OwnAddressType.RESOLVABLE_OR_PUBLIC,
)
async def test_scan_and_connect_after_pairing(
    self, ref_address_type: hci.OwnAddressType
) -> None:
  """Tests scanning remote devices after pairing(IRK exchanged).

  Test steps:
    1. Pair with REF.
    2. Disconnect from REF.
    3. Start advertising on REF.
    4. Start scanning on DUT.
    5. Wait for matched scan result.

  Args:
    ref_address_type: address type of REF device used in advertisements.
  """
  if ref_address_type in (
      hci.OwnAddressType.RESOLVABLE_OR_RANDOM,
      hci.OwnAddressType.RANDOM,
  ):
    identity_address = self.ref.random_address
    identity_address_type = android_constants.AddressTypeStatus.RANDOM
  else:
    identity_address = self.ref.address
    identity_address_type = android_constants.AddressTypeStatus.PUBLIC

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    self.logger.info("[DUT] Pair with REF.")
    await self.le_connect_and_pair(identity_address_type)

    if ref_dut_acl := self.ref.device.find_connection_by_bd_addr(
        hci.Address(self.dut.address, hci.AddressType.PUBLIC_DEVICE),
        core.BT_LE_TRANSPORT,
    ):
      self.logger.info("[REF] Disconnect.")
      with contextlib.suppress(hci.HCI_StatusError):
        await ref_dut_acl.disconnect()
    await dut_cb.wait_for_event(bl4a_api.AclDisconnected)

  self.logger.info("[REF] Start advertising.")
  await self.ref.device.start_advertising(own_address_type=ref_address_type)

  self.logger.info("[DUT] Start scanning for REF.")
  dut_scanner = self.dut.bl4a.start_scanning(
      scan_filter=bl4a_api.ScanFilter(
          device=identity_address,
          address_type=identity_address_type,
      ),
  )
  await dut_scanner.wait_for_event(bl4a_api.ScanResult)
  self.logger.info("[DUT] Found REF, start connecting GATT.")
  await self.dut.bl4a.connect_gatt_client(
      address=identity_address,
      address_type=identity_address_type,
      transport=android_constants.Transport.LE,
  )

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/map_test.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
class MapTest(navi_test_base.TwoDevicesTestBase):

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    if self.dut.device.is_emulator:
      self.dut.setprop(_PROPERTY_MAP_SERVER_ENABLED, "true")

    if self.dut.getprop(_PROPERTY_MAP_SERVER_ENABLED) != "true":
      raise signals.TestAbortClass("MAP server is not enabled on DUT.")

  async def _setup_paired_devices(self) -> None:
    # Setup pairing and terminate connection.
    with self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb:
      await self.classic_connect_and_pair()
      self.dut.bt.setMessageAccessPermission(
          self.ref.address, android_constants.BluetoothAccessPermission.ALLOWED
      )
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  @override
  async def async_setup_test(self) -> None:
    self._clear_sms_messages()
    await super().async_setup_test()
    await self._setup_paired_devices()

  async def _make_mas_client_from_ref(self) -> obex.ClientSession:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Connect to DUT.")
      ref_dut_acl = await self.ref.device.connect(
          self.dut.address, transport=core.BT_BR_EDR_TRANSPORT
      )

      self.logger.info("[REF] Authenticate and encrypt.")
      await ref_dut_acl.authenticate()
      await ref_dut_acl.encrypt()

      self.logger.info("[REF] Find SDP record.")
      sdp_info = await message_access.MasSdpInfo.find(ref_dut_acl)
      if not sdp_info:
        self.fail("Failed to find SDP record for MAP MAS.")

      ref_rfcomm_manager = rfcomm.Manager.find_or_create(self.ref.device)
      self.logger.info("[REF] Connect RFCOMM.")
      rfcomm_client = await ref_rfcomm_manager.connect(ref_dut_acl)
      self.logger.info("[REF] Open DLC to %d.", sdp_info.rfcomm_channel)
      ref_dlc = await rfcomm_client.open_dlc(sdp_info.rfcomm_channel)
      return obex.ClientSession(ref_dlc)

  async def _make_connected_mas_client_from_ref(
      self,
  ) -> tuple[obex.ClientSession, int]:
    with self.dut.bl4a.register_callback(_Module.MAP) as dut_cb:
      client = await self._make_mas_client_from_ref()
      self.logger.info("[REF] Send connect request.")
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        response = await client.send_request(_CONNECT_REQUEST)
      self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
      if not (connection_id := response.headers.connection_id):
        self.fail("Failed to get connection ID from connect response.")
      self.logger.info("[DUT] Wait for profile connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )
    return client, connection_id

  def _clear_sms_messages(self) -> None:
    self.dut.shell(["sqlite3", _MMSSMS_DB_PATH, "'delete from sms'"])

  def _add_sms_message(
      self, sms_message: _SmsMessage, message_type: int
  ) -> None:
    self.dut.shell([
        "sqlite3",
        _MMSSMS_DB_PATH,
        (
            '"insert into sms (thread_id, date, type, address, body) values '
            "(%d, %d, %d, '%s', '%s')\""
            % (
                sms_message.thread_id,
                sms_message.date,
                message_type,
                sms_message.address,
                sms_message.body,
            )
        ),
    ])

  @navi_test_base.parameterized(
      _DisconnectVariant.ACL,
      _DisconnectVariant.BEARER,
  )
  async def test_connect_disconnect(self, variant: _DisconnectVariant) -> None:
    """Tests connecting and disconnecting message_access.

    Test steps:
      1. Connect MAP from REF to DUT.
      2. Disconnect bearer or ACL from REF.

    Args:
      variant: The disconnect variant.
    """
    with self.dut.bl4a.register_callback(_Module.MAP) as dut_cb:
      client, _ = await self._make_connected_mas_client_from_ref()

      match variant:
        case _DisconnectVariant.ACL:
          self.logger.info("[REF] Disconnect ACL.")
          coroutine = (
              client.bearer.multiplexer.l2cap_channel.connection.disconnect()
          )
        case _DisconnectVariant.BEARER:
          self.logger.info("[REF] Disconnect bearer.")
          coroutine = client.bearer.disconnect()

      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        await coroutine

      self.logger.info("[REF] Wait for profile disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
      )

  @navi_test_base.parameterized(
      android_constants.SmsMessageType.INBOX,
      android_constants.SmsMessageType.SENT,
  )
  async def test_get_sms(
      self, message_type: android_constants.SmsMessageType
  ) -> None:
    """Tests getting SMS message from DUT.

    Test steps:
      1. Add SMS message to DUT.
      2. Connect MAP from REF to DUT.
      3. Get SMS message list.
      4. Get SMS message with handle returned in step 3.

    Args:
      message_type: The SMS message type.
    """
    folder_path = {
        android_constants.SmsMessageType.INBOX: "inbox",
        android_constants.SmsMessageType.SENT: "sent",
    }[message_type]
    self._add_sms_message(_SAMPLE_MESSAGE, message_type)

    client, connection_id = await self._make_connected_mas_client_from_ref()
    for folder in ("telecom", "msg"):
      self.logger.info("[REF] Set folder path to %s.", folder)
      setpath_request = obex.SetpathRequest(
          opcode=obex.Opcode.SETPATH,
          final=True,
          flags=obex.SetpathRequest.Flags.DO_NOT_CREATE_FOLDER_IF_NOT_EXIST,
          headers=obex.Headers(
              connection_id=connection_id,
              name=folder,
          ),
      )
      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        response = await client.send_request(setpath_request)
        self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

    self.logger.info("[REF] Get SMS Message list.")
    request = obex.Request(
        opcode=obex.Opcode.GET,
        final=True,
        headers=obex.Headers(
            name=folder_path,
            type=message_access.ObexHeaderType.MESSAGE_LISTING.value,
            connection_id=connection_id,
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
    body = response.headers.body or response.headers.end_of_body or b""
    if (msg := ET.fromstring(body).find("msg")) is None:
      self.fail("Failed to find message in message listing.")
    self.assertIsNotNone(handle := msg.attrib.get("handle"))

    self.logger.info("[REF] Get SMS Message with handle %s.", handle)
    request = obex.Request(
        opcode=obex.Opcode.GET,
        final=True,
        headers=obex.Headers(
            name=handle,
            type=message_access.ObexHeaderType.MESSAGE.value,
            connection_id=connection_id,
            app_parameters=message_access.ApplicationParameters(
                charset=_AppParamValue.Charset.UTF_8,
                attachment=0,
            ).to_bytes(),
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
    body = response.headers.body or response.headers.end_of_body or b""
    lines = body.decode("utf-8").split("\r\n")
    self.assertIn(_SAMPLE_MESSAGE.body, lines)
    self.assertIn(f"TEL:{_SAMPLE_MESSAGE.address}", lines)

  async def test_message_notification(self) -> None:
    """Tests sending message notification from DUT to REF.

    Test steps:
      1. Setup MNS server on REF.
      2. Register message notification from REF.
      3. Wait for DUT to connect to the MNS server.
      4. Add SMS message to DUT.
      5. Wait for REF to receive the message notification.
    """
    ref_rfcomm_manager = rfcomm.Manager.find_or_create(self.ref.device)
    mns_server = message_access.MnsServer(ref_rfcomm_manager)
    self.ref.device.sdp_service_records = {
        1: (
            message_access.MnsSdpInfo(
                service_record_handle=1,
                rfcomm_channel=mns_server.rfcomm_channel,
                version=message_access.Version.V_1_3,
                supported_features=(
                    message_access.ApplicationParameterValue.SupportedFeatures(
                        0xFF
                    )
                ),
            ).to_sdp_records()
        ),
    }
    client, connection_id = await self._make_connected_mas_client_from_ref()

    request = obex.Request(
        opcode=obex.Opcode.PUT,
        final=True,
        headers=obex.Headers(
            type=message_access.ObexHeaderType.NOTIFICATION_REGISTRATION.value,
            connection_id=connection_id,
            app_parameters=message_access.ApplicationParameters(
                notification_status=1
            ).to_bytes(),
            end_of_body=b"\0",
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Register message notification.")
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

    # After sending the request, DUT should connect to the MNS server.
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for MNS OBEX connection.")
      session = await mns_server.sessions.get()
      self.logger.info("[REF] Wait for MNS connect request.")
      await session.connected.wait()

    # SMS Content Observer might not be ready right after MNS connection.
    # However, there isn't a good way to check if the SMS Content Observer is
    # ready. So we just wait for a short period.
    await asyncio.sleep(0.5)

    # Add an SMS message to DUT to trigger message notification.
    new_message = _SmsMessage(
        thread_id=1,
        # Since 24Q1, Android only sends notification for new messages in the
        # last 7 days.
        date=int(self.dut.shell("date +%s")) * 1000,
        address="0987654321",
        body="This is a new message",
    )
    self._add_sms_message(new_message, android_constants.SmsMessageType.INBOX)
    self.dut.bt.notifyMmsSmsChange()
    self.logger.info("[REF] Wait for message notification.")
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      notification = await session.notifications.get()
    # Check the notification content.
    if (event := ET.fromstring(notification).find("event")) is None:
      self.fail("Failed to find event in message notification.")
    self.assertEqual(event.attrib["type"], "NewMessage")
    self.assertEqual(event.attrib["sender_name"], new_message.address)
    self.assertIn("handle", event.attrib)

Tests connecting and disconnecting message_access.

Test steps
  1. Connect MAP from REF to DUT.
  2. Disconnect bearer or ACL from REF.

Parameters:

Name Type Description Default
variant _DisconnectVariant

The disconnect variant.

required
Source code in navi/tests/smoke/map_test.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@navi_test_base.parameterized(
    _DisconnectVariant.ACL,
    _DisconnectVariant.BEARER,
)
async def test_connect_disconnect(self, variant: _DisconnectVariant) -> None:
  """Tests connecting and disconnecting message_access.

  Test steps:
    1. Connect MAP from REF to DUT.
    2. Disconnect bearer or ACL from REF.

  Args:
    variant: The disconnect variant.
  """
  with self.dut.bl4a.register_callback(_Module.MAP) as dut_cb:
    client, _ = await self._make_connected_mas_client_from_ref()

    match variant:
      case _DisconnectVariant.ACL:
        self.logger.info("[REF] Disconnect ACL.")
        coroutine = (
            client.bearer.multiplexer.l2cap_channel.connection.disconnect()
        )
      case _DisconnectVariant.BEARER:
        self.logger.info("[REF] Disconnect bearer.")
        coroutine = client.bearer.disconnect()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await coroutine

    self.logger.info("[REF] Wait for profile disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
    )

Tests getting SMS message from DUT.

Test steps
  1. Add SMS message to DUT.
  2. Connect MAP from REF to DUT.
  3. Get SMS message list.
  4. Get SMS message with handle returned in step 3.

Parameters:

Name Type Description Default
message_type SmsMessageType

The SMS message type.

required
Source code in navi/tests/smoke/map_test.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
@navi_test_base.parameterized(
    android_constants.SmsMessageType.INBOX,
    android_constants.SmsMessageType.SENT,
)
async def test_get_sms(
    self, message_type: android_constants.SmsMessageType
) -> None:
  """Tests getting SMS message from DUT.

  Test steps:
    1. Add SMS message to DUT.
    2. Connect MAP from REF to DUT.
    3. Get SMS message list.
    4. Get SMS message with handle returned in step 3.

  Args:
    message_type: The SMS message type.
  """
  folder_path = {
      android_constants.SmsMessageType.INBOX: "inbox",
      android_constants.SmsMessageType.SENT: "sent",
  }[message_type]
  self._add_sms_message(_SAMPLE_MESSAGE, message_type)

  client, connection_id = await self._make_connected_mas_client_from_ref()
  for folder in ("telecom", "msg"):
    self.logger.info("[REF] Set folder path to %s.", folder)
    setpath_request = obex.SetpathRequest(
        opcode=obex.Opcode.SETPATH,
        final=True,
        flags=obex.SetpathRequest.Flags.DO_NOT_CREATE_FOLDER_IF_NOT_EXIST,
        headers=obex.Headers(
            connection_id=connection_id,
            name=folder,
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(setpath_request)
      self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

  self.logger.info("[REF] Get SMS Message list.")
  request = obex.Request(
      opcode=obex.Opcode.GET,
      final=True,
      headers=obex.Headers(
          name=folder_path,
          type=message_access.ObexHeaderType.MESSAGE_LISTING.value,
          connection_id=connection_id,
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
  body = response.headers.body or response.headers.end_of_body or b""
  if (msg := ET.fromstring(body).find("msg")) is None:
    self.fail("Failed to find message in message listing.")
  self.assertIsNotNone(handle := msg.attrib.get("handle"))

  self.logger.info("[REF] Get SMS Message with handle %s.", handle)
  request = obex.Request(
      opcode=obex.Opcode.GET,
      final=True,
      headers=obex.Headers(
          name=handle,
          type=message_access.ObexHeaderType.MESSAGE.value,
          connection_id=connection_id,
          app_parameters=message_access.ApplicationParameters(
              charset=_AppParamValue.Charset.UTF_8,
              attachment=0,
          ).to_bytes(),
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
  body = response.headers.body or response.headers.end_of_body or b""
  lines = body.decode("utf-8").split("\r\n")
  self.assertIn(_SAMPLE_MESSAGE.body, lines)
  self.assertIn(f"TEL:{_SAMPLE_MESSAGE.address}", lines)

Tests sending message notification from DUT to REF.

Test steps
  1. Setup MNS server on REF.
  2. Register message notification from REF.
  3. Wait for DUT to connect to the MNS server.
  4. Add SMS message to DUT.
  5. Wait for REF to receive the message notification.
Source code in navi/tests/smoke/map_test.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
async def test_message_notification(self) -> None:
  """Tests sending message notification from DUT to REF.

  Test steps:
    1. Setup MNS server on REF.
    2. Register message notification from REF.
    3. Wait for DUT to connect to the MNS server.
    4. Add SMS message to DUT.
    5. Wait for REF to receive the message notification.
  """
  ref_rfcomm_manager = rfcomm.Manager.find_or_create(self.ref.device)
  mns_server = message_access.MnsServer(ref_rfcomm_manager)
  self.ref.device.sdp_service_records = {
      1: (
          message_access.MnsSdpInfo(
              service_record_handle=1,
              rfcomm_channel=mns_server.rfcomm_channel,
              version=message_access.Version.V_1_3,
              supported_features=(
                  message_access.ApplicationParameterValue.SupportedFeatures(
                      0xFF
                  )
              ),
          ).to_sdp_records()
      ),
  }
  client, connection_id = await self._make_connected_mas_client_from_ref()

  request = obex.Request(
      opcode=obex.Opcode.PUT,
      final=True,
      headers=obex.Headers(
          type=message_access.ObexHeaderType.NOTIFICATION_REGISTRATION.value,
          connection_id=connection_id,
          app_parameters=message_access.ApplicationParameters(
              notification_status=1
          ).to_bytes(),
          end_of_body=b"\0",
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[REF] Register message notification.")
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

  # After sending the request, DUT should connect to the MNS server.
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for MNS OBEX connection.")
    session = await mns_server.sessions.get()
    self.logger.info("[REF] Wait for MNS connect request.")
    await session.connected.wait()

  # SMS Content Observer might not be ready right after MNS connection.
  # However, there isn't a good way to check if the SMS Content Observer is
  # ready. So we just wait for a short period.
  await asyncio.sleep(0.5)

  # Add an SMS message to DUT to trigger message notification.
  new_message = _SmsMessage(
      thread_id=1,
      # Since 24Q1, Android only sends notification for new messages in the
      # last 7 days.
      date=int(self.dut.shell("date +%s")) * 1000,
      address="0987654321",
      body="This is a new message",
  )
  self._add_sms_message(new_message, android_constants.SmsMessageType.INBOX)
  self.dut.bt.notifyMmsSmsChange()
  self.logger.info("[REF] Wait for message notification.")
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    notification = await session.notifications.get()
  # Check the notification content.
  if (event := ET.fromstring(notification).find("event")) is None:
    self.fail("Failed to find event in message notification.")
  self.assertEqual(event.attrib["type"], "NewMessage")
  self.assertEqual(event.attrib["sender_name"], new_message.address)
  self.assertIn("handle", event.attrib)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/opp_test.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
class OppTest(navi_test_base.TwoDevicesTestBase):
  ref_opp_server: opp.Server

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    if self.dut.getprop(android_constants.Property.OPP_ENABLED) != 'true':
      raise signals.TestAbortClass('OPP is not enabled on DUT.')

    # Disable Better Bug to avoid unexpected popups.
    with contextlib.suppress(adb.AdbError):
      self.dut.shell([
          'pm',
          'disable-user',
          '--user',
          f'{self.dut.adb.current_user_id}',
          'com.google.android.apps.internal.betterbug',
      ])

    # TODO: Wait for scrcpy fixed.
    # Stay awake during the test.
    self.dut.shell('svc power stayon true')
    # Dismiss the keyguard.
    self.dut.shell('wm dismiss-keyguard')

    await self._setup_paired_devices()

  @retry.retry_on_exception()
  async def _setup_paired_devices(self) -> None:
    # Reset devices.
    self.assertTrue(self.dut.bt.enable())
    self.dut.bt.waitForAdapterState(android_constants.AdapterState.ON)
    self.dut.bt.factoryReset()
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await self.ref.reset()

    # Set up OPP server on REF.
    rfcomm_server = rfcomm.Server(self.ref.device)
    self.ref_opp_server = opp.Server(rfcomm_server)
    self.ref.device.sdp_service_records = {
        _OPP_SERVICE_RECORD_HANDLE: opp.make_sdp_records(
            opp.SdpInfo(
                service_record_handle=_OPP_SERVICE_RECORD_HANDLE,
                rfcomm_channel=self.ref_opp_server.rfcomm_channel,
                profile_version=opp.Version.V_1_2,
            )
        )
    }

    # Setup pairing and terminate connection.
    with self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb:
      await self.classic_connect_and_pair()
      # Wait for ACL disconnection (since there isn't any active profile, it
      # should be disconnected immediately).
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  @override
  async def async_teardown_class(self) -> None:
    await super().async_teardown_class()
    # Stop staying awake during the test.
    self.dut.shell('svc power stayon false')

  @override
  @retry.retry_on_exception()
  async def async_setup_test(self) -> None:
    # Restart Bluetooth on DUT to clear any stale state.
    self.assertTrue(self.dut.bt.disable())
    self.dut.bt.waitForAdapterState(android_constants.AdapterState.OFF)
    self.assertTrue(self.dut.bt.enable())
    self.dut.bt.waitForAdapterState(android_constants.AdapterState.ON)

  async def _make_opp_client_from_ref(self) -> opp.Client:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info('[REF] Connect to DUT.')
      ref_dut_acl = await self.ref.device.connect(
          self.dut.address, transport=core.BT_BR_EDR_TRANSPORT
      )
      self.logger.info('[REF] Authenticate and encrypt.')
      await ref_dut_acl.authenticate()
      await ref_dut_acl.encrypt()

      self.logger.info('[REF] Find SDP record.')
      sdp_info = await opp.find_sdp_record(ref_dut_acl)
      if not sdp_info:
        self.fail('Failed to find SDP record for OPP.')

      self.logger.info('[REF] Connect to OPP.')
      rfcomm_client = await rfcomm.Client(ref_dut_acl).start()
      ref_dlc = await rfcomm_client.open_dlc(sdp_info.rfcomm_channel)
      return opp.Client(ref_dlc)

  async def test_outbound_single_file(self) -> None:
    """Tests sending a single file from DUT to REF.

    Test steps:
      1. Generate a test file on DUT.
      2. Set a random alias to avoid collision with other tests.
      3. Send a sharing file intent from DUT.
      4. Select the target device on DUT.
      5. Wait for OPP connection on REF.
      6. Wait for file transfer to complete on REF.
      7. Check the received file on REF.
    """
    user_id = self.dut.adb.current_user_id
    # [DUT] Generate a test file.
    with tempfile.NamedTemporaryFile(
        mode='wb',
        # On Windows, NamedTemporaryFile cannot be deleted if used multiple
        # times.
        delete=(sys.platform != 'win32'),
    ) as temp_file:
      temp_file.write(_TEST_DATA)
      self.dut.adb.push(
          [temp_file.name, f'/data/media/{user_id}/opp_test_file.txt']
      )

    # [DUT] Set a random alias to avoid collision with other tests.
    self.dut.bt.setAlias(self.ref.address, str(uuid.uuid4()))

    self.logger.info('[DUT] Send sharing file intent.')
    # The file path is different here:
    #  - /storage/ is accessible for Android apps.
    #  - /data/media/ is accessible for adb.
    self.dut.bt.oppShareFiles(
        ['/storage/self/primary/opp_test_file.txt'], _TEST_FILE_MIME_TYPE
    )

    self.logger.info('[DUT] Select the target device')
    # After receiving the sharing file intent, OPP service will pop a Device
    # Selector Activity, showing all available devices with their alias names.
    ui_result = await asyncio.to_thread(
        lambda: self.dut.ui(
            text=self.dut.bt.getAlias(self.ref.address)
        ).wait.click(timeout=_UI_TIMEOUT)
    )
    self.assertTrue(ui_result)

    self.logger.info('[REF] Wait for OPP connection.')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      opp_server_connection = await self.ref_opp_server.wait_connection()

    self.logger.info('[REF] Wait file transfer to complete.')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      received_file = await opp_server_connection.completed_sessions.get()

    # [REF] Check the received file.
    self.assertEqual(received_file.name, 'opp_test_file.txt')
    self.assertStartsWith(received_file.file_type, _TEST_FILE_MIME_TYPE)
    self.assertEqual(received_file.body, _TEST_DATA)

  async def test_inbound_single_file(self) -> None:
    """Tests sending a single file from REF to DUT.

    Test steps:
      1. Connect ACL to DUT.
      2. Find SDP record for OPP.
      3. Connect OPP to DUT.
      4. Start file transfer from REF.
      5. Accept file transfer on DUT.
      6. Wait for file transfer to complete on REF.
    """
    user_id = self.dut.adb.current_user_id
    file_name = 'opp_test_file.txt'
    file_name_pattern_android = (
        f'/data/media/{user_id}/Download/opp_test_file*.txt'
    )
    # Make sure there isn't any similar file on DUT.
    with contextlib.suppress(adb.AdbError):
      self.dut.shell(
          f'test -f {file_name_pattern_android} && '
          f'rm {file_name_pattern_android}'
      )

    opp_client = await self._make_opp_client_from_ref()
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await opp_client.connect(count=1)

    self.logger.info('[REF] Start file transfer.')
    transfer_task = asyncio.create_task(
        opp_client.transmit_file(
            file_name=file_name,
            file_content=_TEST_DATA,
            file_type=_TEST_FILE_MIME_TYPE,
        )
    )
    self.logger.info('[DUT] Accept file transfer.')
    # A notification will be popped up on DUT when there is an incoming file
    # transfer request. We need to click the notification to pop a dialog, and
    # then click the ACCEPT button to accept the file transfer.
    ui_result = await asyncio.to_thread(
        lambda: self.dut.ui(text=file_name).wait.click(timeout=_UI_TIMEOUT)
    )
    self.assertTrue(ui_result)
    ui_result = await asyncio.to_thread(
        lambda: self.dut.ui(
            text='ACCEPT',
            clickable=True,
        ).wait.click(timeout=_UI_TIMEOUT)
    )
    self.assertTrue(ui_result)

    self.logger.info('[REF] Wait file transfer to complete.')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await transfer_task

    @retry.retry_on_exception()
    def check_file_on_dut() -> None:
      # Android always generate a new file name with timestamp, so we need to
      # find the file with the same prefix.
      dut_file_path = self.dut.shell(['ls', file_name_pattern_android])
      with tempfile.TemporaryDirectory() as temp_dir:
        self.dut.adb.pull([dut_file_path, temp_dir])
        with open(
            pathlib.Path(temp_dir, pathlib.Path(dut_file_path).name), 'rb'
        ) as f:
          self.assertEqual(f.read(), _TEST_DATA)

    check_file_on_dut()

  async def test_inbound_transfer_reject(self) -> None:
    """Tests sending files from REF to DUT and reject the transfer on DUT.

    Test steps:
      1. Connect ACL to DUT.
      2. Find SDP record for OPP.
      3. Connect OPP to DUT.
      4. Start file transfer from REF.
      5. Reject file transfer on DUT.
      6. Wait for file transfer to complete on REF.
    """
    file_name = 'opp_test_file.txt'

    opp_client = await self._make_opp_client_from_ref()
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await opp_client.connect(count=1)

    self.logger.info('[REF] Start file transfer.')
    transfer_task = asyncio.create_task(
        opp_client.transmit_file(
            file_name=file_name,
            file_content=_TEST_DATA,
            file_type=_TEST_FILE_MIME_TYPE,
        )
    )
    self.logger.info('[DUT] Reject file transfer.')
    ui_result = await asyncio.to_thread(
        lambda: self.dut.ui(text=file_name).wait.click(timeout=_UI_TIMEOUT)
    )
    self.assertTrue(ui_result)
    ui_result = await asyncio.to_thread(
        lambda: self.dut.ui(
            text='DECLINE',
            clickable=True,
        ).wait.click(timeout=_UI_TIMEOUT)
    )
    self.assertTrue(ui_result)

    self.logger.info('[REF] Wait file transfer to complete.')
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      with self.assertRaises(opp.OppError) as e:
        await transfer_task
      self.assertEqual(e.exception.error_code, obex.ResponseCode.FORBIDDEN)

Tests sending a single file from REF to DUT.

Test steps
  1. Connect ACL to DUT.
  2. Find SDP record for OPP.
  3. Connect OPP to DUT.
  4. Start file transfer from REF.
  5. Accept file transfer on DUT.
  6. Wait for file transfer to complete on REF.
Source code in navi/tests/smoke/opp_test.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
async def test_inbound_single_file(self) -> None:
  """Tests sending a single file from REF to DUT.

  Test steps:
    1. Connect ACL to DUT.
    2. Find SDP record for OPP.
    3. Connect OPP to DUT.
    4. Start file transfer from REF.
    5. Accept file transfer on DUT.
    6. Wait for file transfer to complete on REF.
  """
  user_id = self.dut.adb.current_user_id
  file_name = 'opp_test_file.txt'
  file_name_pattern_android = (
      f'/data/media/{user_id}/Download/opp_test_file*.txt'
  )
  # Make sure there isn't any similar file on DUT.
  with contextlib.suppress(adb.AdbError):
    self.dut.shell(
        f'test -f {file_name_pattern_android} && '
        f'rm {file_name_pattern_android}'
    )

  opp_client = await self._make_opp_client_from_ref()
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    await opp_client.connect(count=1)

  self.logger.info('[REF] Start file transfer.')
  transfer_task = asyncio.create_task(
      opp_client.transmit_file(
          file_name=file_name,
          file_content=_TEST_DATA,
          file_type=_TEST_FILE_MIME_TYPE,
      )
  )
  self.logger.info('[DUT] Accept file transfer.')
  # A notification will be popped up on DUT when there is an incoming file
  # transfer request. We need to click the notification to pop a dialog, and
  # then click the ACCEPT button to accept the file transfer.
  ui_result = await asyncio.to_thread(
      lambda: self.dut.ui(text=file_name).wait.click(timeout=_UI_TIMEOUT)
  )
  self.assertTrue(ui_result)
  ui_result = await asyncio.to_thread(
      lambda: self.dut.ui(
          text='ACCEPT',
          clickable=True,
      ).wait.click(timeout=_UI_TIMEOUT)
  )
  self.assertTrue(ui_result)

  self.logger.info('[REF] Wait file transfer to complete.')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    await transfer_task

  @retry.retry_on_exception()
  def check_file_on_dut() -> None:
    # Android always generate a new file name with timestamp, so we need to
    # find the file with the same prefix.
    dut_file_path = self.dut.shell(['ls', file_name_pattern_android])
    with tempfile.TemporaryDirectory() as temp_dir:
      self.dut.adb.pull([dut_file_path, temp_dir])
      with open(
          pathlib.Path(temp_dir, pathlib.Path(dut_file_path).name), 'rb'
      ) as f:
        self.assertEqual(f.read(), _TEST_DATA)

  check_file_on_dut()

Tests sending files from REF to DUT and reject the transfer on DUT.

Test steps
  1. Connect ACL to DUT.
  2. Find SDP record for OPP.
  3. Connect OPP to DUT.
  4. Start file transfer from REF.
  5. Reject file transfer on DUT.
  6. Wait for file transfer to complete on REF.
Source code in navi/tests/smoke/opp_test.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
async def test_inbound_transfer_reject(self) -> None:
  """Tests sending files from REF to DUT and reject the transfer on DUT.

  Test steps:
    1. Connect ACL to DUT.
    2. Find SDP record for OPP.
    3. Connect OPP to DUT.
    4. Start file transfer from REF.
    5. Reject file transfer on DUT.
    6. Wait for file transfer to complete on REF.
  """
  file_name = 'opp_test_file.txt'

  opp_client = await self._make_opp_client_from_ref()
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    await opp_client.connect(count=1)

  self.logger.info('[REF] Start file transfer.')
  transfer_task = asyncio.create_task(
      opp_client.transmit_file(
          file_name=file_name,
          file_content=_TEST_DATA,
          file_type=_TEST_FILE_MIME_TYPE,
      )
  )
  self.logger.info('[DUT] Reject file transfer.')
  ui_result = await asyncio.to_thread(
      lambda: self.dut.ui(text=file_name).wait.click(timeout=_UI_TIMEOUT)
  )
  self.assertTrue(ui_result)
  ui_result = await asyncio.to_thread(
      lambda: self.dut.ui(
          text='DECLINE',
          clickable=True,
      ).wait.click(timeout=_UI_TIMEOUT)
  )
  self.assertTrue(ui_result)

  self.logger.info('[REF] Wait file transfer to complete.')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    with self.assertRaises(opp.OppError) as e:
      await transfer_task
    self.assertEqual(e.exception.error_code, obex.ResponseCode.FORBIDDEN)

Tests sending a single file from DUT to REF.

Test steps
  1. Generate a test file on DUT.
  2. Set a random alias to avoid collision with other tests.
  3. Send a sharing file intent from DUT.
  4. Select the target device on DUT.
  5. Wait for OPP connection on REF.
  6. Wait for file transfer to complete on REF.
  7. Check the received file on REF.
Source code in navi/tests/smoke/opp_test.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
async def test_outbound_single_file(self) -> None:
  """Tests sending a single file from DUT to REF.

  Test steps:
    1. Generate a test file on DUT.
    2. Set a random alias to avoid collision with other tests.
    3. Send a sharing file intent from DUT.
    4. Select the target device on DUT.
    5. Wait for OPP connection on REF.
    6. Wait for file transfer to complete on REF.
    7. Check the received file on REF.
  """
  user_id = self.dut.adb.current_user_id
  # [DUT] Generate a test file.
  with tempfile.NamedTemporaryFile(
      mode='wb',
      # On Windows, NamedTemporaryFile cannot be deleted if used multiple
      # times.
      delete=(sys.platform != 'win32'),
  ) as temp_file:
    temp_file.write(_TEST_DATA)
    self.dut.adb.push(
        [temp_file.name, f'/data/media/{user_id}/opp_test_file.txt']
    )

  # [DUT] Set a random alias to avoid collision with other tests.
  self.dut.bt.setAlias(self.ref.address, str(uuid.uuid4()))

  self.logger.info('[DUT] Send sharing file intent.')
  # The file path is different here:
  #  - /storage/ is accessible for Android apps.
  #  - /data/media/ is accessible for adb.
  self.dut.bt.oppShareFiles(
      ['/storage/self/primary/opp_test_file.txt'], _TEST_FILE_MIME_TYPE
  )

  self.logger.info('[DUT] Select the target device')
  # After receiving the sharing file intent, OPP service will pop a Device
  # Selector Activity, showing all available devices with their alias names.
  ui_result = await asyncio.to_thread(
      lambda: self.dut.ui(
          text=self.dut.bt.getAlias(self.ref.address)
      ).wait.click(timeout=_UI_TIMEOUT)
  )
  self.assertTrue(ui_result)

  self.logger.info('[REF] Wait for OPP connection.')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    opp_server_connection = await self.ref_opp_server.wait_connection()

  self.logger.info('[REF] Wait file transfer to complete.')
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    received_file = await opp_server_connection.completed_sessions.get()

  # [REF] Check the received file.
  self.assertEqual(received_file.name, 'opp_test_file.txt')
  self.assertStartsWith(received_file.file_type, _TEST_FILE_MIME_TYPE)
  self.assertEqual(received_file.body, _TEST_DATA)

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/pan_test.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class PanTest(navi_test_base.TwoDevicesTestBase):
  panu_enabled: bool
  nap_enabled: bool

  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    self.panu_enabled = self.dut.getprop(_PROPERTY_PAN_PANU_ENABLED) == "true"
    self.nap_enabled = self.dut.getprop(_PROPERTY_PAN_NAP_ENABLED) == "true"

    if not self.panu_enabled and not self.nap_enabled:
      raise signals.TestAbortClass("PANU and NAP are both disabled.")

  async def test_panu_connection(self) -> None:
    """Tests making a PAN connection from REF(PANU) to DUT(NAP).

    When PAN is connected, Tethering service on DUT should properly route
    network traffic to PAN and send some frames like ARP or DHCP.

    Test steps:
      1. Pair DUT and REF.
      2. Connect PAN from DUT(PANU) to REF(NAP).
      3. Wait for Ethernet frame from DUT.
    """
    if not self.panu_enabled:
      self.skipTest("PANU is disabled.")

    ref_pan_connection_result: asyncio.Future[pan.Connection] = (
        asyncio.get_running_loop().create_future()
    )
    ref_pan_server = pan.Server(self.ref.device)
    ref_pan_server.on("connection", ref_pan_connection_result.set_result)
    self.ref.device.sdp_service_records = {
        1: pan.make_panu_service_record(1),
        2: pan.make_gn_service_record(2),
    }
    await self.classic_connect_and_pair()

    with self.dut.bl4a.register_callback(_Module.PAN) as dut_cb:
      self.logger.info("[DUT] Connect PANU.")
      self.dut.bt.setPanConnectionPolicy(
          self.ref.address, android_constants.ConnectionPolicy.ALLOWED
      )

      self.logger.info("[REF] Wait for PANU connection.")
      ref_pan_connection = await ref_pan_connection_result
      # Set up the Ethernet frame sink.
      ref_frame_queue = asyncio.Queue[pan.EthernetFrame]()
      ref_pan_connection.ethernet_sink = ref_frame_queue.put_nowait

      self.logger.info("[DUT] Wait for PANU connection.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

      self.logger.info("[REF] Wait for Ethernet frame from DUT.")
      async with self.assert_not_timeout(_DEFAULT_FRAME_TIMEOUT_SECONDS):
        await ref_frame_queue.get()

  async def test_nap_connection(self) -> None:
    """Tests making a PAN connection from DUT(PANU) to REF(NAP).

    When PAN is connected, Tethering service on DUT should properly route
    network traffic to PAN and send some frames like ARP or DHCP.

    Test steps:
      1. Pair DUT and REF.
      2. Enable NAP on DUT.
      3. Connect PAN from REF(PANU) to DUT(NAP).
      4. Wait for Ethernet frame from DUT.
    """
    if not self.nap_enabled:
      self.skipTest("NAP is disabled.")

    self.ref.device.sdp_service_records = {
        1: pan.make_panu_service_record(1),
        2: pan.make_gn_service_record(2),
    }
    ref_dut_acl = await self.classic_connect_and_pair()

    # Enable NAP on DUT.
    self.dut.bt.setPanTetheringEnabled(True)

    with self.dut.bl4a.register_callback(_Module.PAN) as dut_cb:
      self.logger.info("[DUT] Connect PANU.")
      ref_pan_connection = await pan.Connection.connect(
          ref_dut_acl,
          source_service=core.BT_PANU_SERVICE,
          destination_service=core.BT_NAP_SERVICE,
      )
      ref_frame_queue = asyncio.Queue[pan.EthernetFrame]()
      ref_pan_connection.ethernet_sink = ref_frame_queue.put_nowait

      self.logger.info("[DUT] Wait for PANU connection.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

      self.logger.info("[REF] Wait for Ethernet frame from DUT.")
      async with self.assert_not_timeout(_DEFAULT_FRAME_TIMEOUT_SECONDS):
        await ref_frame_queue.get()

Tests making a PAN connection from DUT(PANU) to REF(NAP).

When PAN is connected, Tethering service on DUT should properly route network traffic to PAN and send some frames like ARP or DHCP.

Test steps
  1. Pair DUT and REF.
  2. Enable NAP on DUT.
  3. Connect PAN from REF(PANU) to DUT(NAP).
  4. Wait for Ethernet frame from DUT.
Source code in navi/tests/smoke/pan_test.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
async def test_nap_connection(self) -> None:
  """Tests making a PAN connection from DUT(PANU) to REF(NAP).

  When PAN is connected, Tethering service on DUT should properly route
  network traffic to PAN and send some frames like ARP or DHCP.

  Test steps:
    1. Pair DUT and REF.
    2. Enable NAP on DUT.
    3. Connect PAN from REF(PANU) to DUT(NAP).
    4. Wait for Ethernet frame from DUT.
  """
  if not self.nap_enabled:
    self.skipTest("NAP is disabled.")

  self.ref.device.sdp_service_records = {
      1: pan.make_panu_service_record(1),
      2: pan.make_gn_service_record(2),
  }
  ref_dut_acl = await self.classic_connect_and_pair()

  # Enable NAP on DUT.
  self.dut.bt.setPanTetheringEnabled(True)

  with self.dut.bl4a.register_callback(_Module.PAN) as dut_cb:
    self.logger.info("[DUT] Connect PANU.")
    ref_pan_connection = await pan.Connection.connect(
        ref_dut_acl,
        source_service=core.BT_PANU_SERVICE,
        destination_service=core.BT_NAP_SERVICE,
    )
    ref_frame_queue = asyncio.Queue[pan.EthernetFrame]()
    ref_pan_connection.ethernet_sink = ref_frame_queue.put_nowait

    self.logger.info("[DUT] Wait for PANU connection.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF] Wait for Ethernet frame from DUT.")
    async with self.assert_not_timeout(_DEFAULT_FRAME_TIMEOUT_SECONDS):
      await ref_frame_queue.get()

Tests making a PAN connection from REF(PANU) to DUT(NAP).

When PAN is connected, Tethering service on DUT should properly route network traffic to PAN and send some frames like ARP or DHCP.

Test steps
  1. Pair DUT and REF.
  2. Connect PAN from DUT(PANU) to REF(NAP).
  3. Wait for Ethernet frame from DUT.
Source code in navi/tests/smoke/pan_test.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
async def test_panu_connection(self) -> None:
  """Tests making a PAN connection from REF(PANU) to DUT(NAP).

  When PAN is connected, Tethering service on DUT should properly route
  network traffic to PAN and send some frames like ARP or DHCP.

  Test steps:
    1. Pair DUT and REF.
    2. Connect PAN from DUT(PANU) to REF(NAP).
    3. Wait for Ethernet frame from DUT.
  """
  if not self.panu_enabled:
    self.skipTest("PANU is disabled.")

  ref_pan_connection_result: asyncio.Future[pan.Connection] = (
      asyncio.get_running_loop().create_future()
  )
  ref_pan_server = pan.Server(self.ref.device)
  ref_pan_server.on("connection", ref_pan_connection_result.set_result)
  self.ref.device.sdp_service_records = {
      1: pan.make_panu_service_record(1),
      2: pan.make_gn_service_record(2),
  }
  await self.classic_connect_and_pair()

  with self.dut.bl4a.register_callback(_Module.PAN) as dut_cb:
    self.logger.info("[DUT] Connect PANU.")
    self.dut.bt.setPanConnectionPolicy(
        self.ref.address, android_constants.ConnectionPolicy.ALLOWED
    )

    self.logger.info("[REF] Wait for PANU connection.")
    ref_pan_connection = await ref_pan_connection_result
    # Set up the Ethernet frame sink.
    ref_frame_queue = asyncio.Queue[pan.EthernetFrame]()
    ref_pan_connection.ethernet_sink = ref_frame_queue.put_nowait

    self.logger.info("[DUT] Wait for PANU connection.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF] Wait for Ethernet frame from DUT.")
    async with self.assert_not_timeout(_DEFAULT_FRAME_TIMEOUT_SECONDS):
      await ref_frame_queue.get()

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/pbap_test.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
class PbapTest(navi_test_base.TwoDevicesTestBase):
  contacts: list[dict[str, Any]]
  call_logs: list[dict[str, Any]]

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()

    if self.dut.device.is_emulator:
      self.dut.setprop(_PROPERTY_PBAP_SERVER_ENABLED, "true")

    if self.dut.getprop(_PROPERTY_PBAP_SERVER_ENABLED) != "true":
      raise signals.TestAbortClass("PBAP server is not enabled on DUT.")
    self.contacts = _CONTACTS
    self.call_logs = _CALL_LOGS

  async def _setup_paired_devices(self) -> None:
    self.ref.device.sdp_service_records = {
        _PBAP_PCE_SDP_RECORD_HANDLE: (
            pbap.PceSdpInfo(
                service_record_handle=_PBAP_PCE_SDP_RECORD_HANDLE,
                version=pbap.Version.V_1_1,
            ).to_sdp_records()
        ),
    }
    with self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb:
      await self.classic_connect_and_pair()
      self.dut.bt.setPhonebookAccessPermission(
          self.ref.address,
          android_constants.BluetoothAccessPermission.ALLOWED,
      )
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=self.ref.address,
              transport=android_constants.Transport.CLASSIC,
          ),
      )

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    self.dut.bt.clearContacts()
    self.dut.bt.clearCallLogs()
    self.dut.bt.addContacts(self.contacts)
    self.dut.bt.addCallLogs(self.call_logs)

    self.ref.device.sdp_service_records = {
        _PBAP_PCE_SDP_RECORD_HANDLE: (
            pbap.PceSdpInfo(
                service_record_handle=_PBAP_PCE_SDP_RECORD_HANDLE,
                version=pbap.Version.V_1_1,
            ).to_sdp_records()
        ),
    }
    await self._setup_paired_devices()

  async def _make_pbap_client_from_ref(self) -> obex.ClientSession:
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Connect to DUT.")
      ref_dut_acl = await self.ref.device.connect(
          self.dut.address, transport=core.BT_BR_EDR_TRANSPORT
      )

      self.logger.info("[REF] Authenticate and encrypt.")
      await ref_dut_acl.authenticate()
      await ref_dut_acl.encrypt()

      self.logger.info("[REF] Find SDP record.")
      sdp_info = await pbap.find_pse_sdp_record(ref_dut_acl)
      if not sdp_info:
        self.fail("Failed to find SDP record for pbap.")

      self.logger.info("[REF] Connect RFCOMM.")
      rfcomm_client = await rfcomm.Client(ref_dut_acl).start()
      self.logger.info("[REF] Open DLC to %d.", sdp_info.rfcomm_channel)
      ref_dlc = await rfcomm_client.open_dlc(sdp_info.rfcomm_channel)
      return obex.ClientSession(ref_dlc)

  @navi_test_base.parameterized(
      _DisconnectVariant.ACL,
      _DisconnectVariant.BEARER,
  )
  async def test_connect_disconnect(self, variant: _DisconnectVariant) -> None:
    """Tests connecting and disconnecting PBAP.

    Test steps:
      1. Connect PBAP from REF to DUT.
      2. Disconnect bearer or ACL from REF.

    Args:
      variant: The disconnect variant.
    """
    with self.dut.bl4a.register_callback(_Module.PBAP) as dut_cb:
      client = await self._make_pbap_client_from_ref()
      self.logger.info("[REF] Send connect request.")
      connect_response = await client.send_request(_CONNECT_REQUEST)
      self.assertEqual(
          connect_response.response_code, obex.ResponseCode.SUCCESS
      )
      self.logger.info("[REF] Wait for profile connected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.CONNECTED,
          ),
      )

      match variant:
        case _DisconnectVariant.ACL:
          self.logger.info("[REF] Disconnect ACL.")
          coroutine = (
              client.bearer.multiplexer.l2cap_channel.connection.disconnect()
          )
        case _DisconnectVariant.BEARER:
          self.logger.info("[REF] Disconnect bearer.")
          coroutine = client.bearer.disconnect()

      async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
        await coroutine

      self.logger.info("[REF] Wait for profile disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
      )

  async def test_download_contact(self) -> None:
    """Tests downloading contact phonebook.

    Test steps:
      1. Connect PBAP from REF to DUT.
      2. Get contact phonebook size.
      3. Get contact phonebook.
    """
    client = await self._make_pbap_client_from_ref()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Send OBEX connect request.")
      connect_response = await client.send_request(_CONNECT_REQUEST)

    self.assertEqual(connect_response.response_code, obex.ResponseCode.SUCCESS)
    connection_id = connect_response.headers.connection_id
    if connection_id is None:
      self.fail("Missing Connection ID.")

    self.logger.info("[REF] Get contact phonebook size.")
    request = obex.Request(
        opcode=obex.Opcode.GET,
        final=True,
        headers=obex.Headers(
            connection_id=connection_id,
            name="telecom/pb.vcf",
            type=_PBAP_PHONE_BOOK_TYPE,
            app_parameters=pbap.ApplicationParameters(
                format=pbap.ApplicationParameterValue.Format.V_3_0,
                max_list_count=0,
                list_start_offset=0,
            ).to_bytes(),
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
    if not response.headers.app_parameters:
      self.fail("Missing app parameters.")
    response_app_params = pbap.ApplicationParameters.from_bytes(
        response.headers.app_parameters
    )
    # The first contact must be the owner, which is not included in the contact
    # list.
    self.assertEqual(response_app_params.phonebook_size, len(self.contacts) + 1)

    self.logger.info("[REF] Get contact phonebook.")
    request = obex.Request(
        opcode=obex.Opcode.GET,
        final=True,
        headers=obex.Headers(
            connection_id=connection_id,
            name="telecom/pb.vcf",
            type=_PBAP_PHONE_BOOK_TYPE,
            app_parameters=pbap.ApplicationParameters(
                format=pbap.ApplicationParameterValue.Format.V_3_0,
                max_list_count=_MAX_LIST_COUNT,
                list_start_offset=0,
            ).to_bytes(),
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

    # Check the vCard list.
    # Note: The order of the vCards is not guaranteed. If the behavior is
    # changed in the future, we need to update the test to ignore the order.
    vcards = _parse_vcard_list(
        response.headers.body or response.headers.end_of_body or b""
    )
    self.logger.debug("<<< %s", vcards)
    self.assertLen(vcards, len(self.contacts) + 1)
    for i, vcard in enumerate(vcards[1:]):
      phone_type = _VCARD_PHONE_TYPES.get(self.contacts[i]["phone_type"])
      email_type = _VCARD_EMAIL_TYPES.get(self.contacts[i]["email_type"])
      self.assertEqual(vcard["FN"], self.contacts[i]["name"])
      self.assertEqual(
          vcard[f"TEL;TYPE={phone_type}"],
          self.contacts[i]["number"].replace("-", ""),
      )
      self.assertEqual(
          vcard[f"EMAIL;TYPE={email_type}"],
          self.contacts[i]["email"],
      )
      org = vcard.get("ORG", None) or vcard.get("ORG;CHARSET=UTF-8", None)
      self.assertEqual(org, self.contacts[i]["company"])

  @navi_test_base.parameterized(
      ("ich",),
      ("och",),
      ("mch",),
  )
  async def test_download_call_logs(self, phonebook_name: str) -> None:
    """Tests downloading call logs."""
    client = await self._make_pbap_client_from_ref()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      self.logger.info("[REF] Send OBEX connect request.")
      connect_response = await client.send_request(_CONNECT_REQUEST)

    self.assertEqual(connect_response.response_code, obex.ResponseCode.SUCCESS)
    connection_id = connect_response.headers.connection_id
    if connection_id is None:
      self.fail("Missing Connection ID.")

    self.logger.info("[REF] Get phonebook.")
    request = obex.Request(
        opcode=obex.Opcode.GET,
        final=True,
        headers=obex.Headers(
            connection_id=connection_id,
            name=f"telecom/{phonebook_name}.vcf",
            type=_PBAP_PHONE_BOOK_TYPE,
            app_parameters=pbap.ApplicationParameters(
                format=pbap.ApplicationParameterValue.Format.V_3_0,
                max_list_count=_MAX_LIST_COUNT,
                list_start_offset=0,
            ).to_bytes(),
        ),
    )
    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      response = await client.send_request(request)
    self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

    call_type = _VCARD_CALL_TYPES.get(phonebook_name)
    call_logs = [
        call_log
        for call_log in self.call_logs
        if call_log["call_type"] == call_type
    ]
    vcards = _parse_vcard_list(
        response.headers.body or response.headers.end_of_body or b""
    )
    self.logger.debug("<<< %s", vcards)
    for i, vcard in enumerate(vcards):
      full_name = vcard.get("FN", None) or vcard.get("FN;CHARSET=UTF-8", None)
      self.assertEqual(full_name, call_logs[i]["name"])
      self.assertEqual(vcard["TEL;TYPE=0"], call_logs[i]["number"])

Tests connecting and disconnecting PBAP.

Test steps
  1. Connect PBAP from REF to DUT.
  2. Disconnect bearer or ACL from REF.

Parameters:

Name Type Description Default
variant _DisconnectVariant

The disconnect variant.

required
Source code in navi/tests/smoke/pbap_test.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
@navi_test_base.parameterized(
    _DisconnectVariant.ACL,
    _DisconnectVariant.BEARER,
)
async def test_connect_disconnect(self, variant: _DisconnectVariant) -> None:
  """Tests connecting and disconnecting PBAP.

  Test steps:
    1. Connect PBAP from REF to DUT.
    2. Disconnect bearer or ACL from REF.

  Args:
    variant: The disconnect variant.
  """
  with self.dut.bl4a.register_callback(_Module.PBAP) as dut_cb:
    client = await self._make_pbap_client_from_ref()
    self.logger.info("[REF] Send connect request.")
    connect_response = await client.send_request(_CONNECT_REQUEST)
    self.assertEqual(
        connect_response.response_code, obex.ResponseCode.SUCCESS
    )
    self.logger.info("[REF] Wait for profile connected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    match variant:
      case _DisconnectVariant.ACL:
        self.logger.info("[REF] Disconnect ACL.")
        coroutine = (
            client.bearer.multiplexer.l2cap_channel.connection.disconnect()
        )
      case _DisconnectVariant.BEARER:
        self.logger.info("[REF] Disconnect bearer.")
        coroutine = client.bearer.disconnect()

    async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
      await coroutine

    self.logger.info("[REF] Wait for profile disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
    )

Tests downloading call logs.

Source code in navi/tests/smoke/pbap_test.py
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
@navi_test_base.parameterized(
    ("ich",),
    ("och",),
    ("mch",),
)
async def test_download_call_logs(self, phonebook_name: str) -> None:
  """Tests downloading call logs."""
  client = await self._make_pbap_client_from_ref()

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[REF] Send OBEX connect request.")
    connect_response = await client.send_request(_CONNECT_REQUEST)

  self.assertEqual(connect_response.response_code, obex.ResponseCode.SUCCESS)
  connection_id = connect_response.headers.connection_id
  if connection_id is None:
    self.fail("Missing Connection ID.")

  self.logger.info("[REF] Get phonebook.")
  request = obex.Request(
      opcode=obex.Opcode.GET,
      final=True,
      headers=obex.Headers(
          connection_id=connection_id,
          name=f"telecom/{phonebook_name}.vcf",
          type=_PBAP_PHONE_BOOK_TYPE,
          app_parameters=pbap.ApplicationParameters(
              format=pbap.ApplicationParameterValue.Format.V_3_0,
              max_list_count=_MAX_LIST_COUNT,
              list_start_offset=0,
          ).to_bytes(),
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

  call_type = _VCARD_CALL_TYPES.get(phonebook_name)
  call_logs = [
      call_log
      for call_log in self.call_logs
      if call_log["call_type"] == call_type
  ]
  vcards = _parse_vcard_list(
      response.headers.body or response.headers.end_of_body or b""
  )
  self.logger.debug("<<< %s", vcards)
  for i, vcard in enumerate(vcards):
    full_name = vcard.get("FN", None) or vcard.get("FN;CHARSET=UTF-8", None)
    self.assertEqual(full_name, call_logs[i]["name"])
    self.assertEqual(vcard["TEL;TYPE=0"], call_logs[i]["number"])

Tests downloading contact phonebook.

Test steps
  1. Connect PBAP from REF to DUT.
  2. Get contact phonebook size.
  3. Get contact phonebook.
Source code in navi/tests/smoke/pbap_test.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
async def test_download_contact(self) -> None:
  """Tests downloading contact phonebook.

  Test steps:
    1. Connect PBAP from REF to DUT.
    2. Get contact phonebook size.
    3. Get contact phonebook.
  """
  client = await self._make_pbap_client_from_ref()

  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    self.logger.info("[REF] Send OBEX connect request.")
    connect_response = await client.send_request(_CONNECT_REQUEST)

  self.assertEqual(connect_response.response_code, obex.ResponseCode.SUCCESS)
  connection_id = connect_response.headers.connection_id
  if connection_id is None:
    self.fail("Missing Connection ID.")

  self.logger.info("[REF] Get contact phonebook size.")
  request = obex.Request(
      opcode=obex.Opcode.GET,
      final=True,
      headers=obex.Headers(
          connection_id=connection_id,
          name="telecom/pb.vcf",
          type=_PBAP_PHONE_BOOK_TYPE,
          app_parameters=pbap.ApplicationParameters(
              format=pbap.ApplicationParameterValue.Format.V_3_0,
              max_list_count=0,
              list_start_offset=0,
          ).to_bytes(),
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)
  if not response.headers.app_parameters:
    self.fail("Missing app parameters.")
  response_app_params = pbap.ApplicationParameters.from_bytes(
      response.headers.app_parameters
  )
  # The first contact must be the owner, which is not included in the contact
  # list.
  self.assertEqual(response_app_params.phonebook_size, len(self.contacts) + 1)

  self.logger.info("[REF] Get contact phonebook.")
  request = obex.Request(
      opcode=obex.Opcode.GET,
      final=True,
      headers=obex.Headers(
          connection_id=connection_id,
          name="telecom/pb.vcf",
          type=_PBAP_PHONE_BOOK_TYPE,
          app_parameters=pbap.ApplicationParameters(
              format=pbap.ApplicationParameterValue.Format.V_3_0,
              max_list_count=_MAX_LIST_COUNT,
              list_start_offset=0,
          ).to_bytes(),
      ),
  )
  async with self.assert_not_timeout(_DEFAULT_TIMEOUT_SECONDS):
    response = await client.send_request(request)
  self.assertEqual(response.response_code, obex.ResponseCode.SUCCESS)

  # Check the vCard list.
  # Note: The order of the vCards is not guaranteed. If the behavior is
  # changed in the future, we need to update the test to ignore the order.
  vcards = _parse_vcard_list(
      response.headers.body or response.headers.end_of_body or b""
  )
  self.logger.debug("<<< %s", vcards)
  self.assertLen(vcards, len(self.contacts) + 1)
  for i, vcard in enumerate(vcards[1:]):
    phone_type = _VCARD_PHONE_TYPES.get(self.contacts[i]["phone_type"])
    email_type = _VCARD_EMAIL_TYPES.get(self.contacts[i]["email_type"])
    self.assertEqual(vcard["FN"], self.contacts[i]["name"])
    self.assertEqual(
        vcard[f"TEL;TYPE={phone_type}"],
        self.contacts[i]["number"].replace("-", ""),
    )
    self.assertEqual(
        vcard[f"EMAIL;TYPE={email_type}"],
        self.contacts[i]["email"],
    )
    org = vcard.get("ORG", None) or vcard.get("ORG;CHARSET=UTF-8", None)
    self.assertEqual(org, self.contacts[i]["company"])

Bases: TwoDevicesTestBase

Source code in navi/tests/smoke/rfcomm_test.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class RfcommTest(navi_test_base.TwoDevicesTestBase):

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()

    # Using highest authentication level to allow secure sockets.
    def pairing_config_factory(
        connection: device.Connection,
    ) -> pairing.PairingConfig:
      del connection  # Unused parameter.
      return pairing.PairingConfig(
          delegate=_PairingDelegate(
              io_capability=(
                  _PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT
              )
          )
      )

    self.ref.device.pairing_config_factory = pairing_config_factory
    # Disable CTKD.
    self.ref.device.l2cap_channel_manager.deregister_fixed_channel(
        smp.SMP_BR_CID
    )
    # Clear SDP records.
    self.ref.device.sdp_service_records.clear()

  @override
  def on_fail(self, record: records.TestResultRecord):
    super().on_fail(record)
    self.dut.reload_snippet()

  async def _setup_pairing(self) -> None:
    ref_dut_acl = await self.classic_connect_and_pair()

    # Terminate ACL connection after pairing.
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      # Disconnection may "fail" if the ACL is already disconnecting or
      # disconnected.
      with contextlib.suppress(core.BaseBumbleError):
        await ref_dut_acl.disconnect()
      self.logger.info("[DUT] Wait for disconnected.")
      await dut_cb.wait_for_event(bl4a_api.AclDisconnected)

    # Wait for 2 seconds to let controllers become idle.
    await asyncio.sleep(datetime.timedelta(seconds=2).total_seconds())

  async def _transmission_test(
      self,
      ref_dut_dlc: rfcomm.DLC,
      dut_ref_dlc: bl4a_api.RfcommChannel,
  ) -> None:
    """Tests transmissting data between DUT and REF over RFCOMM.

    (Not a standalone test.)

    Args:
      ref_dut_dlc: DLC instance of REF, connected to DUT.
      dut_ref_dlc: DLC token of DUT, connected to REF.
    """
    # Store received SDUs in queue.
    ref_sdu_rx_queue = asyncio.Queue[bytes]()
    ref_dut_dlc.sink = ref_sdu_rx_queue.put_nowait

    self.logger.info("Start sending data from REF to DUT")
    async with self.assert_not_timeout(_TRANSMISSION_TIMEOUT_SECONDS):
      ref_dut_dlc.write(_TEST_DATA)
      data_read = await dut_ref_dlc.read(len(_TEST_DATA))
      self.assertEqual(data_read, _TEST_DATA)

    async def ref_rx_task() -> bytearray:
      data_read = bytearray()
      while len(data_read) < len(_TEST_DATA):
        data_read += await ref_sdu_rx_queue.get()
      return data_read

    self.logger.info("Start sending data from DUT to REF")
    async with self.assert_not_timeout(_TRANSMISSION_TIMEOUT_SECONDS):
      data_read, _ = await asyncio.gather(
          ref_rx_task(),
          dut_ref_dlc.write(_TEST_DATA),
      )
      self.assertEqual(data_read, _TEST_DATA)

  @navi_test_base.parameterized(_Variant.SECURE, _Variant.INSECURE)
  async def test_incoming_connection(self, variant: _Variant) -> None:
    """Tests RFCOMM incoming connection, read and write.

    Typical duration: 30-60s.

    Test steps:
      1. Open RFCOMM server on DUT.
      2. Connect ACL from REF to DUT.
      3. Connect RFCOMM from REF to DUT.
      4. Transmit SDU from REF to DUT.
      5. Transmit SDU from DUT to REF.

    Args:
      variant: Whether Secure API is used. (They have the same behavior for
        Bluetooth device in version >=2.1)
    """
    await self._setup_pairing()

    self.logger.info("[DUT] Listen RFCOMM.")
    rfcomm_uuid = str(uuid.uuid4())
    server = self.dut.bl4a.create_rfcomm_server(
        rfcomm_uuid,
        secure=variant == _Variant.SECURE,
    )

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self.ref.device.connect(
        str(self.dut.address),
        transport=core.BT_BR_EDR_TRANSPORT,
    )
    await ref_dut_acl.authenticate()
    await ref_dut_acl.encrypt(True)

    self.logger.info("[REF] Find RFCOMM channel.")
    channel = await rfcomm.find_rfcomm_channel_with_uuid(
        ref_dut_acl, rfcomm_uuid
    )
    if not channel:
      self.fail("Failed to find RFCOMM channel with UUID.")

    self.logger.info("[REF] Connect RFCOMM channel to DUT.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_rfcomm = await rfcomm.Client(ref_dut_acl).start()

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
          ref_rfcomm.open_dlc(channel),
          server.accept(),
      )

    await self._transmission_test(ref_dut_dlc, dut_ref_dlc)

  @navi_test_base.parameterized(_Variant.SECURE, _Variant.INSECURE)
  async def test_outgoing_connection(self, variant: _Variant) -> None:
    """Tests RFCOMM outgoing connection, read and write.

    Typical duration: 30-60s.

    Test steps:
      1. Open RFCOMM server on REF.
      2. Connect RFCOMM from REF to DUT.
      3. Transmit SDU from REF to DUT.
      4. Transmit SDU from DUT to REF.

    Args:
      variant: Whether Secure API is used. (They have the same behavior for
        Bluetooth device in version >=2.1)
    """
    await self._setup_pairing()

    ref_accept_future = asyncio.get_running_loop().create_future()
    channel = rfcomm.Server(self.ref.device).listen(
        acceptor=ref_accept_future.set_result
    )
    self.ref.device.sdp_service_records[_RFCOMM_SERVICE_RECORD_HANDLE] = (
        rfcomm.make_service_sdp_records(
            service_record_handle=_RFCOMM_SERVICE_RECORD_HANDLE,
            channel=channel,
            uuid=core.UUID(_RFCOMM_UUID),
        )
    )

    self.logger.info("[DUT] Connect RFCOMM channel to REF.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
          ref_accept_future,
          self.dut.bl4a.create_rfcomm_channel(
              address=self.ref.address,
              secure=variant == _Variant.SECURE,
              uuid=_RFCOMM_UUID,
          ),
      )

    await self._transmission_test(ref_dut_dlc, dut_ref_dlc)

Tests RFCOMM incoming connection, read and write.

Typical duration: 30-60s.

Test steps
  1. Open RFCOMM server on DUT.
  2. Connect ACL from REF to DUT.
  3. Connect RFCOMM from REF to DUT.
  4. Transmit SDU from REF to DUT.
  5. Transmit SDU from DUT to REF.

Parameters:

Name Type Description Default
variant _Variant

Whether Secure API is used. (They have the same behavior for Bluetooth device in version >=2.1)

required
Source code in navi/tests/smoke/rfcomm_test.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
@navi_test_base.parameterized(_Variant.SECURE, _Variant.INSECURE)
async def test_incoming_connection(self, variant: _Variant) -> None:
  """Tests RFCOMM incoming connection, read and write.

  Typical duration: 30-60s.

  Test steps:
    1. Open RFCOMM server on DUT.
    2. Connect ACL from REF to DUT.
    3. Connect RFCOMM from REF to DUT.
    4. Transmit SDU from REF to DUT.
    5. Transmit SDU from DUT to REF.

  Args:
    variant: Whether Secure API is used. (They have the same behavior for
      Bluetooth device in version >=2.1)
  """
  await self._setup_pairing()

  self.logger.info("[DUT] Listen RFCOMM.")
  rfcomm_uuid = str(uuid.uuid4())
  server = self.dut.bl4a.create_rfcomm_server(
      rfcomm_uuid,
      secure=variant == _Variant.SECURE,
  )

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self.ref.device.connect(
      str(self.dut.address),
      transport=core.BT_BR_EDR_TRANSPORT,
  )
  await ref_dut_acl.authenticate()
  await ref_dut_acl.encrypt(True)

  self.logger.info("[REF] Find RFCOMM channel.")
  channel = await rfcomm.find_rfcomm_channel_with_uuid(
      ref_dut_acl, rfcomm_uuid
  )
  if not channel:
    self.fail("Failed to find RFCOMM channel with UUID.")

  self.logger.info("[REF] Connect RFCOMM channel to DUT.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_rfcomm = await rfcomm.Client(ref_dut_acl).start()

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
        ref_rfcomm.open_dlc(channel),
        server.accept(),
    )

  await self._transmission_test(ref_dut_dlc, dut_ref_dlc)

Tests RFCOMM outgoing connection, read and write.

Typical duration: 30-60s.

Test steps
  1. Open RFCOMM server on REF.
  2. Connect RFCOMM from REF to DUT.
  3. Transmit SDU from REF to DUT.
  4. Transmit SDU from DUT to REF.

Parameters:

Name Type Description Default
variant _Variant

Whether Secure API is used. (They have the same behavior for Bluetooth device in version >=2.1)

required
Source code in navi/tests/smoke/rfcomm_test.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
@navi_test_base.parameterized(_Variant.SECURE, _Variant.INSECURE)
async def test_outgoing_connection(self, variant: _Variant) -> None:
  """Tests RFCOMM outgoing connection, read and write.

  Typical duration: 30-60s.

  Test steps:
    1. Open RFCOMM server on REF.
    2. Connect RFCOMM from REF to DUT.
    3. Transmit SDU from REF to DUT.
    4. Transmit SDU from DUT to REF.

  Args:
    variant: Whether Secure API is used. (They have the same behavior for
      Bluetooth device in version >=2.1)
  """
  await self._setup_pairing()

  ref_accept_future = asyncio.get_running_loop().create_future()
  channel = rfcomm.Server(self.ref.device).listen(
      acceptor=ref_accept_future.set_result
  )
  self.ref.device.sdp_service_records[_RFCOMM_SERVICE_RECORD_HANDLE] = (
      rfcomm.make_service_sdp_records(
          service_record_handle=_RFCOMM_SERVICE_RECORD_HANDLE,
          channel=channel,
          uuid=core.UUID(_RFCOMM_UUID),
      )
  )

  self.logger.info("[DUT] Connect RFCOMM channel to REF.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
        ref_accept_future,
        self.dut.bl4a.create_rfcomm_channel(
            address=self.ref.address,
            secure=variant == _Variant.SECURE,
            uuid=_RFCOMM_UUID,
        ),
    )

  await self._transmission_test(ref_dut_dlc, dut_ref_dlc)