Skip to content

Functionality

Bases: TwoDevicesTestBase

Tests A2DP Sink and AVRCP Controller profiles.

Source code in navi/tests/functionality/a2dp_sink_test.py
 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
class A2dpSinkTest(navi_test_base.TwoDevicesTestBase):
  """Tests A2DP Sink and AVRCP Controller profiles."""

  bluetooth_package: str
  bluetooth_browser_service: str

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

    if self.dut.device.is_emulator:
      self.setprop_for_class_context(_Property.A2DP_SINK_ENABLED, "true")

      self.setprop_for_class_context(_Property.AVRCP_CONTROLLER_ENABLED, "true")

    if self.dut.getprop(_Property.A2DP_SINK_ENABLED) != "true":
      raise signals.TestAbortClass("A2DP Sink is not enabled on DUT.")
    if self.dut.getprop(_Property.AVRCP_CONTROLLER_ENABLED) != "true":
      raise signals.TestAbortClass("AVRCP Controller is not enabled on DUT.")

    # The bluetooth package name might be different on different DUTs.
    component = self.dut.shell(
        "pm query-services -a android.media.browse.MediaBrowserService --brief"
        f" | grep {_MEDIA_BROWSER_SERVICE_NAME}"
    )
    if not component:
      self.fail("No media browser service found")
    self.bluetooth_package, self.bluetooth_browser_service = component.split(
        "/"
    )

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    # Setup SDP service records.
    self.ref.device.sdp_service_records = {
        _A2DP_SERVICE_RECORD_HANDLE: (
            a2dp_ext.SourceSdpRecord(
                _A2DP_SERVICE_RECORD_HANDLE
            ).to_service_attributes()
        ),
        _AVRCP_TARGET_RECORD_HANDLE: (
            avrcp.TargetServiceSdpRecord(
                _AVRCP_TARGET_RECORD_HANDLE,
                supported_features=(
                    avrcp.TargetFeatures.CATEGORY_1
                    | avrcp.TargetFeatures.SUPPORTS_BROWSING
                ),
            ).to_service_attributes()
        ),
    }

  @dataclasses.dataclass
  class SourceDevice:
    avdtp_protocol_queue: asyncio.Queue[avdtp.Protocol]
    avrcp_protocol: avrcp.Protocol
    avrcp_protocol_starts: asyncio.Queue[None]
    browsing_target_queue: asyncio.Queue[avrcp_ext.BrowsingTarget]

  def _setup_a2dp_source_device(
      self,
      bumble_device: device.Device,
      codecs: Sequence[a2dp_ext.A2dpCodec] = (
          a2dp_ext.A2dpCodec.SBC,
          a2dp_ext.A2dpCodec.AAC,
      ),
  ) -> SourceDevice:
    # Setup AVDTP server.
    avdtp_protocol_queue = asyncio.Queue[avdtp.Protocol]()
    avdtp_listener = avdtp.Listener.for_device(device=bumble_device)

    def on_avdtp_connection(protocol: avdtp.Protocol) -> None:
      for codec in codecs:
        protocol.add_source(
            codec.get_default_capabilities(),
            codec.get_media_packet_pump(protocol.l2cap_channel.peer_mtu),
        )
      avdtp_protocol_queue.put_nowait(protocol)

    avdtp_listener.on(avdtp_listener.EVENT_CONNECTION, on_avdtp_connection)
    # Setup AVRCP server.
    avrcp_delegate = avrcp.Delegate()
    avrcp_protocol_starts = asyncio.Queue[None]()
    avrcp_protocol = avrcp.Protocol(delegate=avrcp_delegate)
    avrcp_protocol.listen(bumble_device)
    avrcp_protocol.on(
        avrcp_protocol.EVENT_START,
        lambda: avrcp_protocol_starts.put_nowait(None),
    )
    browsing_target_queue = avrcp_ext.BrowsingTarget.listen(
        bumble_device,
        players=[_SAMPLE_PLAYER],
    )
    return self.SourceDevice(
        avdtp_protocol_queue=avdtp_protocol_queue,
        avrcp_protocol=avrcp_protocol,
        avrcp_protocol_starts=avrcp_protocol_starts,
        browsing_target_queue=browsing_target_queue,
    )

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

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
    """
    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

    dut_a2dp_sink_callback = self.dut.bl4a.register_callback(
        bl4a_api.Module.A2DP_SINK
    )
    self.test_case_context.push(dut_a2dp_sink_callback)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVDTP connection")
      avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
      self.logger.info("[REF] Discover remote endpoints")
      await avdtp_protocol.discover_remote_endpoints()
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    self.logger.info("[DUT] Waiting for A2DP connection state changed.")
    await dut_a2dp_sink_callback.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.CONNECTED,
        )
    )

  @navi_test_base.named_parameterized(
      sbc=dict(
          codec=a2dp_ext.A2dpCodec.SBC, a2dp_codec_type=a2dp.CodecType.SBC
      ),
      aac=dict(
          codec=a2dp_ext.A2dpCodec.AAC,
          a2dp_codec_type=a2dp.CodecType.MPEG_2_4_AAC,
      ),
      opus=dict(
          codec=a2dp_ext.A2dpCodec.OPUS,
          a2dp_codec_type=a2dp.CodecType.NON_A2DP,
          vendor_id=a2dp.OpusMediaCodecInformation.VENDOR_ID,
          codec_id=a2dp.OpusMediaCodecInformation.CODEC_ID,
      ),
  )
  async def test_streaming(
      self,
      codec: a2dp_ext.A2dpCodec,
      a2dp_codec_type: a2dp.CodecType,
      vendor_id: int = 0,
      codec_id: int = 0,
  ) -> None:
    """Tests streaming.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Start streaming.
      4. Stop streaming.

    Args:
      codec: The codec to use for streaming.
      a2dp_codec_type: The codec type to use for streaming.
      vendor_id: The vendor ID of the codec.
      codec_id: The codec ID of the codec.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(
        self.ref.device, codecs=list(set([a2dp_ext.A2dpCodec.SBC, codec]))
    )

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVDTP connection")
      avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
      self.logger.info("[REF] Discover remote endpoints")
      await avdtp_protocol.discover_remote_endpoints()

    sources = a2dp_ext.find_local_endpoints_by_codec(
        avdtp_protocol, a2dp_codec_type, avdtp.LocalSource, vendor_id, codec_id
    )
    if not sources:
      self.fail(f"No A2DP local {codec.name} source found")

    if not (stream := sources[0].stream):
      # If there is only one source, DUT will automatically create a stream.
      self.fail("REF doesn't create a stream")

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Start stream")
      await stream.start()

    await asyncio.sleep(_DEFAULT_STREAM_DURATION_SECONDS)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Stop stream")
      await stream.stop()

  async def test_browsing(self) -> None:
    """Tests AVRCP Browsing.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP, AVRCP and Browsing connection from DUT.
      3. Browse remote devices, players, folders and media elements.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()
      self.logger.info("[REF] Wait for Browsing connection")
      await ref_a2dp_source_device.browsing_target_queue.get()

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)
    self.logger.info("[DUT] Get media browser root id")
    root_id = await browser.get_root_media_item()

    self.logger.info("[DUT] Browse remote devices")
    children = await browser.get_children(root_id)
    self.assertLen(children, 1)
    self.assertEqual(children[0].title, self.ref.device.name)
    device_node = children[0]
    assert device_node.id is not None

    self.logger.info("[DUT] Browse players")
    children = await browser.get_children(device_node.id)
    self.assertLen(children, 1)
    self.assertEqual(children[0].title, _SAMPLE_PLAYER.displayable_name)
    player_node = children[0]
    assert player_node.id is not None

    self.logger.info("[DUT] Browse folders")
    children = await browser.get_children(player_node.id)
    self.assertLen(children, 1)
    self.assertEqual(children[0].title, _SAMPLE_FOLDER.displayable_name)
    folder_node = children[0]
    assert folder_node.id is not None

    self.logger.info("[DUT] Browse media elements")
    children = await browser.get_children(folder_node.id)
    self.assertLen(children, 1)
    self.assertEqual(children[0].title, _SAMPLE_TRACK.displayable_name)

  @navi_test_base.TwoDevicesTestBase.require_flag(
      "com.android.bluetooth.flags.avrcp_controller_abs_vol_changed_notification"
  )
  async def test_set_volume_from_dut(self) -> None:
    """Tests AVRCP set absolute volume.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Set volume from DUT.
      4. Verify the volume is changed on DUT and REF.
    """

    if self.dut.bt.isVolumeFixed():
      self.skipTest("Volume is fixed by manager")
    if _FEATURE_AUTOMOTIVE in self.dut.shell("pm list features"):
      self.skipTest("Volume is fixed on automotive")

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    volume_iter = ref_avrcp_protocol.monitor_volume()
    audio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO)
    self.test_case_context.push(audio_cb)

    dut_min_volume = self.dut.bt.getMinVolume(_STREAM_TYPE_MUSIC)
    dut_max_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_MUSIC)

    for volume in range(dut_min_volume, dut_max_volume + 1):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        if self.dut.bt.getVolume(_STREAM_TYPE_MUSIC) == volume:
          continue

        if self.dut.bluetooth_mainline_version < 361611000:
          ref_expected_volume = (
              volume * avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
          ) // dut_max_volume
        else:
          ref_expected_volume = int(
              decimal.Decimal(
                  (volume * avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME)
                  / dut_max_volume
              ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
          )

        self.logger.info("[REF] Wait for volume interim")
        await anext(volume_iter)

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

        self.logger.info("[DUT] Wait for volume changed")
        await audio_cb.wait_for_event(
            bl4a_api.VolumeChanged(
                stream_type=_STREAM_TYPE_MUSIC,
                volume_value=volume,
            )
        )
        self.logger.info("[REF] Wait for volume changed")
        self.assertEqual(await anext(volume_iter), ref_expected_volume)

  async def test_set_volume_from_ref(self) -> None:
    """Tests AVRCP set absolute volume.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Set volume from REF.
      4. Verify the volume is changed on DUT.
    """

    if self.dut.bt.isVolumeFixed():
      self.skipTest("Volume is fixed by manager")
    if _FEATURE_AUTOMOTIVE in self.dut.shell("pm list features"):
      self.skipTest("Volume is fixed on automotive")

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    audio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO)
    self.test_case_context.push(audio_cb)

    dut_max_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_MUSIC)

    for volume in range(
        0, avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME + 1, 8
    ):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        if self.dut.bluetooth_mainline_version < 361611000:
          dut_expected_volume = (
              volume * dut_max_volume
          ) // avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
        else:
          dut_expected_volume = int(
              decimal.Decimal(
                  (volume * dut_max_volume)
                  / avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
              ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
          )

        if self.dut.bt.getVolume(_STREAM_TYPE_MUSIC) == dut_expected_volume:
          continue

        self.logger.info("[REF] Set volume to %d", volume)
        await ref_avrcp_protocol.send_avrcp_command(
            avc.CommandFrame.CommandType.CONTROL,
            avrcp.SetAbsoluteVolumeCommand(volume),
        )

        self.logger.info("[DUT] Wait for volume changed")
        await audio_cb.wait_for_event(
            bl4a_api.VolumeChanged(
                stream_type=_STREAM_TYPE_MUSIC,
                volume_value=dut_expected_volume,
            )
        )

  async def test_playback_control(self) -> None:
    """Tests AVRCP playback control.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Play media on DUT and check if REF receives the PLAY command.
      4. Pause media on DUT and check if REF receives the PAUSE command.
      5. Stop media on DUT and check if REF receives the STOP command.
      6. Skip to next media on DUT and check if REF receives the FORWARD
      command.
      7. Skip to previous media on DUT and check if REF receives the BACKWARD
         command.
      8. Fast forward media on DUT and check if REF receives the FAST_FORWARD
         command.
      9. Rewind media on DUT and check if REF receives the REWIND command.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(
        self.ref.device, codecs=[a2dp_ext.A2dpCodec.SBC]
    )

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVDTP connection")
      avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
      self.logger.info("[REF] Discover remote endpoints")
      await avdtp_protocol.discover_remote_endpoints()
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    key_events = asyncio.Queue[tuple[avc.PassThroughFrame.OperationId, bool]]()

    class Delegate(avrcp.Delegate):

      @override
      async def on_key_event(
          self,
          key: avc.PassThroughFrame.OperationId,
          pressed: bool,
          data: bytes,
      ) -> None:
        key_events.put_nowait((key, pressed))

    ref_avrcp_protocol.delegate = Delegate()

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)

    for playback_control_method, expected_key in [
        (browser.play, avc.PassThroughFrame.OperationId.PLAY),
        (browser.pause, avc.PassThroughFrame.OperationId.PAUSE),
        (browser.stop, avc.PassThroughFrame.OperationId.STOP),
        (browser.skip_to_next, avc.PassThroughFrame.OperationId.FORWARD),
        (browser.skip_to_previous, avc.PassThroughFrame.OperationId.BACKWARD),
    ]:
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[DUT] %s", playback_control_method.__name__)
        playback_control_method()
        self.logger.info("[REF] Wait for %r pressed", expected_key)
        actual_key, actual_pressed = await key_events.get()
        self.assertTrue(actual_pressed)
        self.assertEqual(actual_key, expected_key)
        self.logger.info("[REF] Wait for %r released", expected_key)
        actual_key, actual_pressed = await key_events.get()
        self.assertFalse(actual_pressed)
        self.assertEqual(actual_key, expected_key)

    # Holdable keys, need to call twice to release the key.
    for playback_control_method, expected_key in [
        (browser.fast_forward, avc.PassThroughFrame.OperationId.FAST_FORWARD),
        (browser.rewind, avc.PassThroughFrame.OperationId.REWIND),
    ]:
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[DUT] %s", playback_control_method.__name__)
        playback_control_method()
        self.logger.info("[REF] Wait for %r pressed", expected_key)
        actual_key, actual_pressed = await key_events.get()
        self.assertTrue(actual_pressed)
        self.assertEqual(actual_key, expected_key)

        self.logger.info("[DUT] %s", playback_control_method.__name__)
        playback_control_method()
        self.logger.info("[REF] Wait for %r released", expected_key)
        actual_key, actual_pressed = await key_events.get()
        self.assertFalse(actual_pressed)
        self.assertEqual(actual_key, expected_key)

  async def test_playback_status(self) -> None:
    """Tests AVRCP playback status.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Play media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED
         event with PLAYING state.
      4. Pause media on DUT and check if REF receives the
         PLAYBACK_STATUS_CHANGED event with PAUSED state.
      5. Stop media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED
         event with STOPPED state.
      6. Fast forward media on DUT and check if REF receives the
         PLAYBACK_STATUS_CHANGED event with FAST_FORWARD state.
      7. Rewind media on DUT and check if REF receives the
          PLAYBACK_STATUS_CHANGED event with REWIND state.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(
        self.ref.device, codecs=[a2dp_ext.A2dpCodec.SBC]
    )
    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    ref_avrcp_protocol.delegate = avrcp.Delegate(
        supported_events=[avrcp.EventId(avrcp.EventId.PLAYBACK_STATUS_CHANGED)]
    )

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)
    self.logger.info("[DUT] Register media controller callback")
    callback = browser.register_callback()
    self.test_case_context.push(callback)

    for ref_playback_status, dut_playback_state in [
        (
            avrcp.PlayStatus.PLAYING,
            android_constants.MediaPlaybackState.PLAYING,
        ),
        (avrcp.PlayStatus.PAUSED, android_constants.MediaPlaybackState.PAUSED),
        (
            avrcp.PlayStatus.STOPPED,
            android_constants.MediaPlaybackState.STOPPED,
        ),
        (
            avrcp.PlayStatus.FWD_SEEK,
            android_constants.MediaPlaybackState.FAST_FORWARDING,
        ),
        (
            avrcp.PlayStatus.REV_SEEK,
            android_constants.MediaPlaybackState.REWINDING,
        ),
    ]:
      ref_playback_status = avrcp.PlayStatus(ref_playback_status)

      self.logger.info(
          "[REF] Notify playback status changed to %r", ref_playback_status
      )
      ref_avrcp_protocol.delegate.playback_status = ref_playback_status
      ref_avrcp_protocol.notify_playback_status_changed(ref_playback_status)

      self.logger.info("[DUT] Wait for playback state changed event")
      await callback.wait_for_event(
          bl4a_api.MediaBrowser.PlaybackStateChanged(state=dut_playback_state)
      )

  async def test_media_metadata(self) -> None:
    """Tests AVRCP media metadata changed and get item attributes.

    Android passively receives the media metadata changed event and actively
    requests the media metadata using get item attributes when the media is
    changed.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Set now playing item on REF.
      4. Verify the media metadata is changed on DUT.
      5. Get item attributes on DUT and verify the media metadata.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    ref_avrcp_protocol.delegate = avrcp.Delegate(
        supported_events=[
            avrcp.EventId.TRACK_CHANGED,  # pytype: disable=wrong-arg-types
            avrcp.EventId.AVAILABLE_PLAYERS_CHANGED,  # pytype: disable=wrong-arg-types
        ]
    )

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()
      self.logger.info("[REF] Wait for Browsing connection")
      ref_browsing_target = (
          await ref_a2dp_source_device.browsing_target_queue.get()
      )

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)
    self.logger.info("[DUT] Register media controller callback")
    callback = browser.register_callback()
    self.test_case_context.push(callback)

    self.logger.info("[REF] Set browsed player")
    ref_browsed_player = ref_browsing_target.browsed_player = (
        ref_browsing_target.players[0]
    )
    self.logger.info("[REF] Set now playing item")
    ref_browsed_player.now_playing_items = [_SAMPLE_TRACK]
    ref_avrcp_protocol.delegate.current_track_uid = (
        _SAMPLE_TRACK.media_element_uid
    )
    ref_avrcp_protocol.notify_track_changed(_SAMPLE_TRACK.media_element_uid)

    self.logger.info("[DUT] Wait for metadata changed event")
    await callback.wait_for_event(
        bl4a_api.MediaBrowser.MetadataChanged(
            title=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.TITLE],
            artist=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.ARTIST_NAME],
            album=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.ALBUM_NAME],
        )
    )

  async def test_player_app_setting_changed_from_ref(self) -> None:
    """Tests AVRCP player app setting changed from REF.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Set player app setting on REF and check if DUT receives the
         PLAYER_APPLICATION_SETTING_CHANGED event.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    condition = asyncio.Condition()

    class Delegate(avrcp.Delegate):

      @override
      async def get_current_player_app_settings(
          self,
      ) -> dict[avrcp.ApplicationSetting.AttributeId, int]:
        # Whenever event notification of PLAYER_APPLICATION_SETTING_CHANGED is
        # registered, the delegate.get_current_player_app_settings() is called
        # to get the current player app settings.
        async with condition:
          condition.notify_all()
          return await super().get_current_player_app_settings()

    ref_avrcp_protocol.delegate = Delegate(
        supported_events=[
            avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED,  # pytype: disable=wrong-arg-types
        ],
        supported_player_app_settings={
            avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: [
                avrcp.ApplicationSetting.RepeatModeStatus.OFF,
                avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
                avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
                avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
            ],
            avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: [
                avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
                avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
                avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
            ],
        },
    )
    ref_avrcp_protocol.delegate.player_app_settings = {
        avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: (
            avrcp.ApplicationSetting.RepeatModeStatus.OFF
        ),
        avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF
        ),
    }

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)
    self.logger.info("[DUT] Register media controller callback")
    callback = browser.register_callback()
    self.test_case_context.push(callback)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    async def wait_for_registration() -> None:
      async with (
          self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
          condition,
      ):
        await condition.wait_for(
            lambda: avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED
            in ref_avrcp_protocol.notification_listeners
        )

    for ref_repeat_mode, dut_repeat_mode in [
        (
            avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
            android_constants.RepeatMode.ALL,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
            android_constants.RepeatMode.ONE,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
            android_constants.RepeatMode.GROUP,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.OFF,
            android_constants.RepeatMode.OFF,
        ),
    ]:
      await wait_for_registration()
      self.logger.info("[REF] Set repeat mode to %r", ref_repeat_mode)
      ref_avrcp_protocol.delegate.player_app_settings[
          avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
      ] = ref_repeat_mode
      ref_avrcp_protocol.notify_player_application_settings_changed([
          avrcp.PlayerApplicationSettingChangedEvent.Setting(
              avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,  # pytype: disable=wrong-arg-types
              ref_repeat_mode,
          )
      ])
      self.logger.info(
          "[DUT] Wait for repeat mode changed to %r", dut_repeat_mode
      )
      await callback.wait_for_event(
          bl4a_api.MediaBrowser.RepeatModeChanged(mode=dut_repeat_mode)
      )

    for ref_shuffle_mode, dut_shuffle_mode in [
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
            android_constants.ShuffleMode.ALL,
        ),
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
            android_constants.ShuffleMode.GROUP,
        ),
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
            android_constants.ShuffleMode.OFF,
        ),
    ]:
      await wait_for_registration()
      self.logger.info("[REF] Set shuffle mode to %r", ref_shuffle_mode)
      ref_avrcp_protocol.delegate.player_app_settings[
          avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF
      ] = ref_shuffle_mode
      ref_avrcp_protocol.notify_player_application_settings_changed([
          avrcp.PlayerApplicationSettingChangedEvent.Setting(
              avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,  # pytype: disable=wrong-arg-types
              ref_shuffle_mode,
          )
      ])
      self.logger.info(
          "[DUT] Wait for shuffle mode changed to %r", dut_shuffle_mode
      )
      await callback.wait_for_event(
          bl4a_api.MediaBrowser.ShuffleModeChanged(mode=dut_shuffle_mode)
      )

  async def test_player_app_setting_changed_from_dut(self) -> None:
    """Tests AVRCP player app setting changed from DUT.

    Test steps:
      1. Connect and pair REF.
      2. Make A2DP and AVRCP connection from DUT.
      3. Set player app setting on DUT and check if REF receives the
         PLAYER_APPLICATION_SETTING_CHANGED event.
    """

    ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
    ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
    condition = asyncio.Condition()

    class Delegate(avrcp.Delegate):

      @override
      async def set_player_app_settings(
          self, attribute: avrcp.ApplicationSetting.AttributeId, value: int
      ) -> None:
        await super().set_player_app_settings(attribute, value)
        async with condition:
          condition.notify_all()

    ref_avrcp_protocol.delegate = Delegate(
        supported_events=[
            avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED,  # pytype: disable=wrong-arg-types
        ],
        supported_player_app_settings={
            avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: [
                avrcp.ApplicationSetting.RepeatModeStatus.OFF,
                avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
                avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
                avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
            ],
            avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: [
                avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
                avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
                avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
            ],
        },
    )
    ref_avrcp_protocol.delegate.player_app_settings = {
        avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: (
            avrcp.ApplicationSetting.RepeatModeStatus.OFF
        ),
        avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF
        ),
    }

    self.logger.info("[DUT] Connect to media browser")
    browser = self.dut.bl4a.connect_media_browser(
        self.bluetooth_package,
        self.bluetooth_browser_service,
    )
    self.test_case_context.push(browser)

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

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Wait for AVRCP connection")
      await ref_a2dp_source_device.avrcp_protocol_starts.get()

    for ref_repeat_mode, dut_repeat_mode in [
        (
            avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
            android_constants.RepeatMode.ALL,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
            android_constants.RepeatMode.ONE,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
            android_constants.RepeatMode.GROUP,
        ),
        (
            avrcp.ApplicationSetting.RepeatModeStatus.OFF,
            android_constants.RepeatMode.OFF,
        ),
    ]:
      self.logger.info("[DUT] Set repeat mode to %r", dut_repeat_mode)
      browser.set_repeat_mode(dut_repeat_mode)
      self.logger.info(
          "[REF] Wait for repeat mode changed to %r", ref_repeat_mode
      )
      async with (
          self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
          condition,
      ):
        await condition.wait_for(
            lambda: (
                ref_avrcp_protocol.delegate.player_app_settings.get(
                    avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
                )
                == ref_repeat_mode,  # pylint: disable=cell-var-from-loop
            )
        )

    for ref_shuffle_mode, due_shuffle_mode in [
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
            android_constants.ShuffleMode.ALL,
        ),
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
            android_constants.ShuffleMode.GROUP,
        ),
        (
            avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
            android_constants.ShuffleMode.OFF,
        ),
    ]:
      self.logger.info("[DUT] Set shuffle mode to %r", due_shuffle_mode)
      browser.set_shuffle_mode(due_shuffle_mode)
      self.logger.info(
          "[REF] Wait for shuffle mode changed to %r", ref_shuffle_mode
      )
      async with (
          self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
          condition,
      ):
        await condition.wait_for(
            lambda: (
                ref_avrcp_protocol.delegate.player_app_settings.get(
                    avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF
                )
                == ref_shuffle_mode  # pylint: disable=cell-var-from-loop
            ),
        )

Tests AVRCP Browsing.

Test steps
  1. Connect and pair REF.
  2. Make A2DP, AVRCP and Browsing connection from DUT.
  3. Browse remote devices, players, folders and media elements.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_browsing(self) -> None:
  """Tests AVRCP Browsing.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP, AVRCP and Browsing connection from DUT.
    3. Browse remote devices, players, folders and media elements.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()
    self.logger.info("[REF] Wait for Browsing connection")
    await ref_a2dp_source_device.browsing_target_queue.get()

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)
  self.logger.info("[DUT] Get media browser root id")
  root_id = await browser.get_root_media_item()

  self.logger.info("[DUT] Browse remote devices")
  children = await browser.get_children(root_id)
  self.assertLen(children, 1)
  self.assertEqual(children[0].title, self.ref.device.name)
  device_node = children[0]
  assert device_node.id is not None

  self.logger.info("[DUT] Browse players")
  children = await browser.get_children(device_node.id)
  self.assertLen(children, 1)
  self.assertEqual(children[0].title, _SAMPLE_PLAYER.displayable_name)
  player_node = children[0]
  assert player_node.id is not None

  self.logger.info("[DUT] Browse folders")
  children = await browser.get_children(player_node.id)
  self.assertLen(children, 1)
  self.assertEqual(children[0].title, _SAMPLE_FOLDER.displayable_name)
  folder_node = children[0]
  assert folder_node.id is not None

  self.logger.info("[DUT] Browse media elements")
  children = await browser.get_children(folder_node.id)
  self.assertLen(children, 1)
  self.assertEqual(children[0].title, _SAMPLE_TRACK.displayable_name)

Tests AVRCP media metadata changed and get item attributes.

Android passively receives the media metadata changed event and actively requests the media metadata using get item attributes when the media is changed.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Set now playing item on REF.
  4. Verify the media metadata is changed on DUT.
  5. Get item attributes on DUT and verify the media metadata.
Source code in navi/tests/functionality/a2dp_sink_test.py
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_media_metadata(self) -> None:
  """Tests AVRCP media metadata changed and get item attributes.

  Android passively receives the media metadata changed event and actively
  requests the media metadata using get item attributes when the media is
  changed.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Set now playing item on REF.
    4. Verify the media metadata is changed on DUT.
    5. Get item attributes on DUT and verify the media metadata.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  ref_avrcp_protocol.delegate = avrcp.Delegate(
      supported_events=[
          avrcp.EventId.TRACK_CHANGED,  # pytype: disable=wrong-arg-types
          avrcp.EventId.AVAILABLE_PLAYERS_CHANGED,  # pytype: disable=wrong-arg-types
      ]
  )

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()
    self.logger.info("[REF] Wait for Browsing connection")
    ref_browsing_target = (
        await ref_a2dp_source_device.browsing_target_queue.get()
    )

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)
  self.logger.info("[DUT] Register media controller callback")
  callback = browser.register_callback()
  self.test_case_context.push(callback)

  self.logger.info("[REF] Set browsed player")
  ref_browsed_player = ref_browsing_target.browsed_player = (
      ref_browsing_target.players[0]
  )
  self.logger.info("[REF] Set now playing item")
  ref_browsed_player.now_playing_items = [_SAMPLE_TRACK]
  ref_avrcp_protocol.delegate.current_track_uid = (
      _SAMPLE_TRACK.media_element_uid
  )
  ref_avrcp_protocol.notify_track_changed(_SAMPLE_TRACK.media_element_uid)

  self.logger.info("[DUT] Wait for metadata changed event")
  await callback.wait_for_event(
      bl4a_api.MediaBrowser.MetadataChanged(
          title=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.TITLE],
          artist=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.ARTIST_NAME],
          album=_SAMPLE_TRACK.attributes[avrcp.MediaAttributeId.ALBUM_NAME],
      )
  )

Tests A2DP connection establishment right after a pairing session.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_paired_connect_outgoing(self) -> None:
  """Tests A2DP connection establishment right after a pairing session.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
  """
  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

  dut_a2dp_sink_callback = self.dut.bl4a.register_callback(
      bl4a_api.Module.A2DP_SINK
  )
  self.test_case_context.push(dut_a2dp_sink_callback)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVDTP connection")
    avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
    self.logger.info("[REF] Discover remote endpoints")
    await avdtp_protocol.discover_remote_endpoints()
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  self.logger.info("[DUT] Waiting for A2DP connection state changed.")
  await dut_a2dp_sink_callback.wait_for_event(
      bl4a_api.ProfileConnectionStateChanged(
          address=self.ref.address,
          state=android_constants.ConnectionState.CONNECTED,
      )
  )

Tests AVRCP playback control.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Play media on DUT and check if REF receives the PLAY command.
  4. Pause media on DUT and check if REF receives the PAUSE command.
  5. Stop media on DUT and check if REF receives the STOP command.
  6. Skip to next media on DUT and check if REF receives the FORWARD command.
  7. Skip to previous media on DUT and check if REF receives the BACKWARD command.
  8. Fast forward media on DUT and check if REF receives the FAST_FORWARD command.
  9. Rewind media on DUT and check if REF receives the REWIND command.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_playback_control(self) -> None:
  """Tests AVRCP playback control.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Play media on DUT and check if REF receives the PLAY command.
    4. Pause media on DUT and check if REF receives the PAUSE command.
    5. Stop media on DUT and check if REF receives the STOP command.
    6. Skip to next media on DUT and check if REF receives the FORWARD
    command.
    7. Skip to previous media on DUT and check if REF receives the BACKWARD
       command.
    8. Fast forward media on DUT and check if REF receives the FAST_FORWARD
       command.
    9. Rewind media on DUT and check if REF receives the REWIND command.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(
      self.ref.device, codecs=[a2dp_ext.A2dpCodec.SBC]
  )

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVDTP connection")
    avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
    self.logger.info("[REF] Discover remote endpoints")
    await avdtp_protocol.discover_remote_endpoints()
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  key_events = asyncio.Queue[tuple[avc.PassThroughFrame.OperationId, bool]]()

  class Delegate(avrcp.Delegate):

    @override
    async def on_key_event(
        self,
        key: avc.PassThroughFrame.OperationId,
        pressed: bool,
        data: bytes,
    ) -> None:
      key_events.put_nowait((key, pressed))

  ref_avrcp_protocol.delegate = Delegate()

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)

  for playback_control_method, expected_key in [
      (browser.play, avc.PassThroughFrame.OperationId.PLAY),
      (browser.pause, avc.PassThroughFrame.OperationId.PAUSE),
      (browser.stop, avc.PassThroughFrame.OperationId.STOP),
      (browser.skip_to_next, avc.PassThroughFrame.OperationId.FORWARD),
      (browser.skip_to_previous, avc.PassThroughFrame.OperationId.BACKWARD),
  ]:
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[DUT] %s", playback_control_method.__name__)
      playback_control_method()
      self.logger.info("[REF] Wait for %r pressed", expected_key)
      actual_key, actual_pressed = await key_events.get()
      self.assertTrue(actual_pressed)
      self.assertEqual(actual_key, expected_key)
      self.logger.info("[REF] Wait for %r released", expected_key)
      actual_key, actual_pressed = await key_events.get()
      self.assertFalse(actual_pressed)
      self.assertEqual(actual_key, expected_key)

  # Holdable keys, need to call twice to release the key.
  for playback_control_method, expected_key in [
      (browser.fast_forward, avc.PassThroughFrame.OperationId.FAST_FORWARD),
      (browser.rewind, avc.PassThroughFrame.OperationId.REWIND),
  ]:
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[DUT] %s", playback_control_method.__name__)
      playback_control_method()
      self.logger.info("[REF] Wait for %r pressed", expected_key)
      actual_key, actual_pressed = await key_events.get()
      self.assertTrue(actual_pressed)
      self.assertEqual(actual_key, expected_key)

      self.logger.info("[DUT] %s", playback_control_method.__name__)
      playback_control_method()
      self.logger.info("[REF] Wait for %r released", expected_key)
      actual_key, actual_pressed = await key_events.get()
      self.assertFalse(actual_pressed)
      self.assertEqual(actual_key, expected_key)

Tests AVRCP playback status.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Play media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED event with PLAYING state.
  4. Pause media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED event with PAUSED state.
  5. Stop media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED event with STOPPED state.
  6. Fast forward media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED event with FAST_FORWARD state.
  7. Rewind media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED event with REWIND state.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_playback_status(self) -> None:
  """Tests AVRCP playback status.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Play media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED
       event with PLAYING state.
    4. Pause media on DUT and check if REF receives the
       PLAYBACK_STATUS_CHANGED event with PAUSED state.
    5. Stop media on DUT and check if REF receives the PLAYBACK_STATUS_CHANGED
       event with STOPPED state.
    6. Fast forward media on DUT and check if REF receives the
       PLAYBACK_STATUS_CHANGED event with FAST_FORWARD state.
    7. Rewind media on DUT and check if REF receives the
        PLAYBACK_STATUS_CHANGED event with REWIND state.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(
      self.ref.device, codecs=[a2dp_ext.A2dpCodec.SBC]
  )
  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  ref_avrcp_protocol.delegate = avrcp.Delegate(
      supported_events=[avrcp.EventId(avrcp.EventId.PLAYBACK_STATUS_CHANGED)]
  )

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)
  self.logger.info("[DUT] Register media controller callback")
  callback = browser.register_callback()
  self.test_case_context.push(callback)

  for ref_playback_status, dut_playback_state in [
      (
          avrcp.PlayStatus.PLAYING,
          android_constants.MediaPlaybackState.PLAYING,
      ),
      (avrcp.PlayStatus.PAUSED, android_constants.MediaPlaybackState.PAUSED),
      (
          avrcp.PlayStatus.STOPPED,
          android_constants.MediaPlaybackState.STOPPED,
      ),
      (
          avrcp.PlayStatus.FWD_SEEK,
          android_constants.MediaPlaybackState.FAST_FORWARDING,
      ),
      (
          avrcp.PlayStatus.REV_SEEK,
          android_constants.MediaPlaybackState.REWINDING,
      ),
  ]:
    ref_playback_status = avrcp.PlayStatus(ref_playback_status)

    self.logger.info(
        "[REF] Notify playback status changed to %r", ref_playback_status
    )
    ref_avrcp_protocol.delegate.playback_status = ref_playback_status
    ref_avrcp_protocol.notify_playback_status_changed(ref_playback_status)

    self.logger.info("[DUT] Wait for playback state changed event")
    await callback.wait_for_event(
        bl4a_api.MediaBrowser.PlaybackStateChanged(state=dut_playback_state)
    )

Tests AVRCP player app setting changed from DUT.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Set player app setting on DUT and check if REF receives the PLAYER_APPLICATION_SETTING_CHANGED event.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_player_app_setting_changed_from_dut(self) -> None:
  """Tests AVRCP player app setting changed from DUT.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Set player app setting on DUT and check if REF receives the
       PLAYER_APPLICATION_SETTING_CHANGED event.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  condition = asyncio.Condition()

  class Delegate(avrcp.Delegate):

    @override
    async def set_player_app_settings(
        self, attribute: avrcp.ApplicationSetting.AttributeId, value: int
    ) -> None:
      await super().set_player_app_settings(attribute, value)
      async with condition:
        condition.notify_all()

  ref_avrcp_protocol.delegate = Delegate(
      supported_events=[
          avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED,  # pytype: disable=wrong-arg-types
      ],
      supported_player_app_settings={
          avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: [
              avrcp.ApplicationSetting.RepeatModeStatus.OFF,
              avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
              avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
              avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
          ],
          avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: [
              avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
              avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
              avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
          ],
      },
  )
  ref_avrcp_protocol.delegate.player_app_settings = {
      avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: (
          avrcp.ApplicationSetting.RepeatModeStatus.OFF
      ),
      avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF
      ),
  }

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  for ref_repeat_mode, dut_repeat_mode in [
      (
          avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
          android_constants.RepeatMode.ALL,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
          android_constants.RepeatMode.ONE,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
          android_constants.RepeatMode.GROUP,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.OFF,
          android_constants.RepeatMode.OFF,
      ),
  ]:
    self.logger.info("[DUT] Set repeat mode to %r", dut_repeat_mode)
    browser.set_repeat_mode(dut_repeat_mode)
    self.logger.info(
        "[REF] Wait for repeat mode changed to %r", ref_repeat_mode
    )
    async with (
        self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
        condition,
    ):
      await condition.wait_for(
          lambda: (
              ref_avrcp_protocol.delegate.player_app_settings.get(
                  avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
              )
              == ref_repeat_mode,  # pylint: disable=cell-var-from-loop
          )
      )

  for ref_shuffle_mode, due_shuffle_mode in [
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
          android_constants.ShuffleMode.ALL,
      ),
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
          android_constants.ShuffleMode.GROUP,
      ),
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
          android_constants.ShuffleMode.OFF,
      ),
  ]:
    self.logger.info("[DUT] Set shuffle mode to %r", due_shuffle_mode)
    browser.set_shuffle_mode(due_shuffle_mode)
    self.logger.info(
        "[REF] Wait for shuffle mode changed to %r", ref_shuffle_mode
    )
    async with (
        self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
        condition,
    ):
      await condition.wait_for(
          lambda: (
              ref_avrcp_protocol.delegate.player_app_settings.get(
                  avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF
              )
              == ref_shuffle_mode  # pylint: disable=cell-var-from-loop
          ),
      )

Tests AVRCP player app setting changed from REF.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Set player app setting on REF and check if DUT receives the PLAYER_APPLICATION_SETTING_CHANGED event.
Source code in navi/tests/functionality/a2dp_sink_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
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
async def test_player_app_setting_changed_from_ref(self) -> None:
  """Tests AVRCP player app setting changed from REF.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Set player app setting on REF and check if DUT receives the
       PLAYER_APPLICATION_SETTING_CHANGED event.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)
  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  condition = asyncio.Condition()

  class Delegate(avrcp.Delegate):

    @override
    async def get_current_player_app_settings(
        self,
    ) -> dict[avrcp.ApplicationSetting.AttributeId, int]:
      # Whenever event notification of PLAYER_APPLICATION_SETTING_CHANGED is
      # registered, the delegate.get_current_player_app_settings() is called
      # to get the current player app settings.
      async with condition:
        condition.notify_all()
        return await super().get_current_player_app_settings()

  ref_avrcp_protocol.delegate = Delegate(
      supported_events=[
          avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED,  # pytype: disable=wrong-arg-types
      ],
      supported_player_app_settings={
          avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: [
              avrcp.ApplicationSetting.RepeatModeStatus.OFF,
              avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
              avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
              avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
          ],
          avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: [
              avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
              avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
              avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
          ],
      },
  )
  ref_avrcp_protocol.delegate.player_app_settings = {
      avrcp.ApplicationSetting.AttributeId.REPEAT_MODE: (
          avrcp.ApplicationSetting.RepeatModeStatus.OFF
      ),
      avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF: (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF
      ),
  }

  self.logger.info("[DUT] Connect to media browser")
  browser = self.dut.bl4a.connect_media_browser(
      self.bluetooth_package,
      self.bluetooth_browser_service,
  )
  self.test_case_context.push(browser)
  self.logger.info("[DUT] Register media controller callback")
  callback = browser.register_callback()
  self.test_case_context.push(callback)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  async def wait_for_registration() -> None:
    async with (
        self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS),
        condition,
    ):
      await condition.wait_for(
          lambda: avrcp.EventId.PLAYER_APPLICATION_SETTING_CHANGED
          in ref_avrcp_protocol.notification_listeners
      )

  for ref_repeat_mode, dut_repeat_mode in [
      (
          avrcp.ApplicationSetting.RepeatModeStatus.ALL_TRACK_REPEAT,
          android_constants.RepeatMode.ALL,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.SINGLE_TRACK_REPEAT,
          android_constants.RepeatMode.ONE,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.GROUP_REPEAT,
          android_constants.RepeatMode.GROUP,
      ),
      (
          avrcp.ApplicationSetting.RepeatModeStatus.OFF,
          android_constants.RepeatMode.OFF,
      ),
  ]:
    await wait_for_registration()
    self.logger.info("[REF] Set repeat mode to %r", ref_repeat_mode)
    ref_avrcp_protocol.delegate.player_app_settings[
        avrcp.ApplicationSetting.AttributeId.REPEAT_MODE
    ] = ref_repeat_mode
    ref_avrcp_protocol.notify_player_application_settings_changed([
        avrcp.PlayerApplicationSettingChangedEvent.Setting(
            avrcp.ApplicationSetting.AttributeId.REPEAT_MODE,  # pytype: disable=wrong-arg-types
            ref_repeat_mode,
        )
    ])
    self.logger.info(
        "[DUT] Wait for repeat mode changed to %r", dut_repeat_mode
    )
    await callback.wait_for_event(
        bl4a_api.MediaBrowser.RepeatModeChanged(mode=dut_repeat_mode)
    )

  for ref_shuffle_mode, dut_shuffle_mode in [
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.ALL_TRACKS_SHUFFLE,
          android_constants.ShuffleMode.ALL,
      ),
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.GROUP_SHUFFLE,
          android_constants.ShuffleMode.GROUP,
      ),
      (
          avrcp.ApplicationSetting.ShuffleOnOffStatus.OFF,
          android_constants.ShuffleMode.OFF,
      ),
  ]:
    await wait_for_registration()
    self.logger.info("[REF] Set shuffle mode to %r", ref_shuffle_mode)
    ref_avrcp_protocol.delegate.player_app_settings[
        avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF
    ] = ref_shuffle_mode
    ref_avrcp_protocol.notify_player_application_settings_changed([
        avrcp.PlayerApplicationSettingChangedEvent.Setting(
            avrcp.ApplicationSetting.AttributeId.SHUFFLE_ON_OFF,  # pytype: disable=wrong-arg-types
            ref_shuffle_mode,
        )
    ])
    self.logger.info(
        "[DUT] Wait for shuffle mode changed to %r", dut_shuffle_mode
    )
    await callback.wait_for_event(
        bl4a_api.MediaBrowser.ShuffleModeChanged(mode=dut_shuffle_mode)
    )

Tests AVRCP set absolute volume.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Set volume from DUT.
  4. Verify the volume is changed on DUT and REF.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
@navi_test_base.TwoDevicesTestBase.require_flag(
    "com.android.bluetooth.flags.avrcp_controller_abs_vol_changed_notification"
)
async def test_set_volume_from_dut(self) -> None:
  """Tests AVRCP set absolute volume.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Set volume from DUT.
    4. Verify the volume is changed on DUT and REF.
  """

  if self.dut.bt.isVolumeFixed():
    self.skipTest("Volume is fixed by manager")
  if _FEATURE_AUTOMOTIVE in self.dut.shell("pm list features"):
    self.skipTest("Volume is fixed on automotive")

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  volume_iter = ref_avrcp_protocol.monitor_volume()
  audio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO)
  self.test_case_context.push(audio_cb)

  dut_min_volume = self.dut.bt.getMinVolume(_STREAM_TYPE_MUSIC)
  dut_max_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_MUSIC)

  for volume in range(dut_min_volume, dut_max_volume + 1):
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      if self.dut.bt.getVolume(_STREAM_TYPE_MUSIC) == volume:
        continue

      if self.dut.bluetooth_mainline_version < 361611000:
        ref_expected_volume = (
            volume * avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
        ) // dut_max_volume
      else:
        ref_expected_volume = int(
            decimal.Decimal(
                (volume * avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME)
                / dut_max_volume
            ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
        )

      self.logger.info("[REF] Wait for volume interim")
      await anext(volume_iter)

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

      self.logger.info("[DUT] Wait for volume changed")
      await audio_cb.wait_for_event(
          bl4a_api.VolumeChanged(
              stream_type=_STREAM_TYPE_MUSIC,
              volume_value=volume,
          )
      )
      self.logger.info("[REF] Wait for volume changed")
      self.assertEqual(await anext(volume_iter), ref_expected_volume)

Tests AVRCP set absolute volume.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Set volume from REF.
  4. Verify the volume is changed on DUT.
Source code in navi/tests/functionality/a2dp_sink_test.py
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
async def test_set_volume_from_ref(self) -> None:
  """Tests AVRCP set absolute volume.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Set volume from REF.
    4. Verify the volume is changed on DUT.
  """

  if self.dut.bt.isVolumeFixed():
    self.skipTest("Volume is fixed by manager")
  if _FEATURE_AUTOMOTIVE in self.dut.shell("pm list features"):
    self.skipTest("Volume is fixed on automotive")

  ref_a2dp_source_device = self._setup_a2dp_source_device(self.ref.device)

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVRCP connection")
    await ref_a2dp_source_device.avrcp_protocol_starts.get()

  ref_avrcp_protocol = ref_a2dp_source_device.avrcp_protocol
  audio_cb = self.dut.bl4a.register_callback(bl4a_api.Module.AUDIO)
  self.test_case_context.push(audio_cb)

  dut_max_volume = self.dut.bt.getMaxVolume(_STREAM_TYPE_MUSIC)

  for volume in range(
      0, avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME + 1, 8
  ):
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      if self.dut.bluetooth_mainline_version < 361611000:
        dut_expected_volume = (
            volume * dut_max_volume
        ) // avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
      else:
        dut_expected_volume = int(
            decimal.Decimal(
                (volume * dut_max_volume)
                / avrcp.SetAbsoluteVolumeCommand.MAXIMUM_VOLUME
            ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
        )

      if self.dut.bt.getVolume(_STREAM_TYPE_MUSIC) == dut_expected_volume:
        continue

      self.logger.info("[REF] Set volume to %d", volume)
      await ref_avrcp_protocol.send_avrcp_command(
          avc.CommandFrame.CommandType.CONTROL,
          avrcp.SetAbsoluteVolumeCommand(volume),
      )

      self.logger.info("[DUT] Wait for volume changed")
      await audio_cb.wait_for_event(
          bl4a_api.VolumeChanged(
              stream_type=_STREAM_TYPE_MUSIC,
              volume_value=dut_expected_volume,
          )
      )

Tests streaming.

Test steps
  1. Connect and pair REF.
  2. Make A2DP and AVRCP connection from DUT.
  3. Start streaming.
  4. Stop streaming.

Parameters:

Name Type Description Default
codec A2dpCodec

The codec to use for streaming.

required
a2dp_codec_type CodecType

The codec type to use for streaming.

required
vendor_id int

The vendor ID of the codec.

0
codec_id int

The codec ID of the codec.

0
Source code in navi/tests/functionality/a2dp_sink_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
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
@navi_test_base.named_parameterized(
    sbc=dict(
        codec=a2dp_ext.A2dpCodec.SBC, a2dp_codec_type=a2dp.CodecType.SBC
    ),
    aac=dict(
        codec=a2dp_ext.A2dpCodec.AAC,
        a2dp_codec_type=a2dp.CodecType.MPEG_2_4_AAC,
    ),
    opus=dict(
        codec=a2dp_ext.A2dpCodec.OPUS,
        a2dp_codec_type=a2dp.CodecType.NON_A2DP,
        vendor_id=a2dp.OpusMediaCodecInformation.VENDOR_ID,
        codec_id=a2dp.OpusMediaCodecInformation.CODEC_ID,
    ),
)
async def test_streaming(
    self,
    codec: a2dp_ext.A2dpCodec,
    a2dp_codec_type: a2dp.CodecType,
    vendor_id: int = 0,
    codec_id: int = 0,
) -> None:
  """Tests streaming.

  Test steps:
    1. Connect and pair REF.
    2. Make A2DP and AVRCP connection from DUT.
    3. Start streaming.
    4. Stop streaming.

  Args:
    codec: The codec to use for streaming.
    a2dp_codec_type: The codec type to use for streaming.
    vendor_id: The vendor ID of the codec.
    codec_id: The codec ID of the codec.
  """

  ref_a2dp_source_device = self._setup_a2dp_source_device(
      self.ref.device, codecs=list(set([a2dp_ext.A2dpCodec.SBC, codec]))
  )

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

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Wait for AVDTP connection")
    avdtp_protocol = await ref_a2dp_source_device.avdtp_protocol_queue.get()
    self.logger.info("[REF] Discover remote endpoints")
    await avdtp_protocol.discover_remote_endpoints()

  sources = a2dp_ext.find_local_endpoints_by_codec(
      avdtp_protocol, a2dp_codec_type, avdtp.LocalSource, vendor_id, codec_id
  )
  if not sources:
    self.fail(f"No A2DP local {codec.name} source found")

  if not (stream := sources[0].stream):
    # If there is only one source, DUT will automatically create a stream.
    self.fail("REF doesn't create a stream")

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Start stream")
    await stream.start()

  await asyncio.sleep(_DEFAULT_STREAM_DURATION_SECONDS)

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Stop stream")
    await stream.stop()

Bases: TwoDevicesTestBase

A2DP Source (DUT) tests.

Source code in navi/tests/functionality/a2dp_source_test.py
 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
class A2dpSourceTest(navi_test_base.TwoDevicesTestBase):
  """A2DP Source (DUT) tests."""

  @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 Source is not enabled on DUT.")

  def _setup_a2dp_sink_from_ref(
      self, codecs: list[_A2dpCodec]
  ) -> avdtp.Listener:
    """Sets up A2DP Sink profile on REF.

    Args:
      codecs: A2DP codecs supported by REF.

    Returns:
      An avdtp.Listener.
    """
    self.logger.info("[REF]setup_a2dp_sink_from_ref")
    listener = a2dp_ext.setup_sink_server(
        self.ref.device,
        [codec.get_default_capabilities() for codec in codecs],
        _A2DP_SERVICE_RECORD_HANDLE,
    )

    return listener

  async def _pair_and_connect_from_dut(self) -> None:
    """Tests A2DP connection establishment right after a pairing session."""
    with self.dut.bl4a.register_callback(_Module.A2DP) as dut_cb:
      self._setup_a2dp_sink_from_ref([_A2dpCodec.SBC])
      self.logger.info("[DUT] Connect and pair REF.")
      await self.classic_connect_and_pair(connect_profiles=True)

      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,
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      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 _find_or_connect_acl_from_ref(
      self, dut_address: str
  ) -> bumble_device.Connection:
    """Finds or creates an ACL connection from REF to DUT."""
    if not (
        dut_ref_acl := self.ref.device.find_connection_by_bd_addr(
            hci.Address(dut_address)
        )
    ):
      dut_ref_acl = await self.ref.device.connect(
          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()
    return dut_ref_acl

  async def _connect_a2dp_from_dut(self, ref_address: str) -> None:
    """Initiates A2DP connection from DUT to REF."""
    self.logger.info("[DUT] Initiating A2DP connection.")
    self.dut.bt.connect(ref_address)

  async def _connect_a2dp_from_ref(
      self, dut_ref_acl: bumble_device.Connection
  ) -> None:
    """Initiates A2DP (AVDTP) connection from REF to DUT."""
    self.logger.info("[REF] Initiating AVDTP connection.")
    await avdtp.Protocol.connect(dut_ref_acl)

  async def test_paired_connect_a2dp_simultaneous(self) -> None:
    """Tests A2DP connection establishment with simultaneous connection.

    Test steps:
      1. Setup pairing between DUT(A2DP Source) and REF(A2DP Sink).
      2. Terminate ACL connection from DUT.
      3. Setup ACL connection from REF.
      4. Trigger A2DP connection from DUT and REF at same time.
      5. Wait A2DP connected on DUT.
      6. Disconnect from DUT.
      7. Wait A2DP disconnected on DUT.

    Test Results:
      DUT should be able to establish A2DP connection successfully even in
      conflicting scenarios.
    """

    with self.dut.bl4a.register_callback(_Module.A2DP) as dut_cb:
      # Step 1: Setup pairing and initial A2DP connection
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[DUT] Setup pairing and initial A2DP connection.",
      ):
        await self._pair_and_connect_from_dut()

      # Step 2: Terminate ACL connection
      await self.disconnect_with_check(
          self.ref.address, android_constants.Transport.CLASSIC
      )

      # Step 3: Setup ACL connection from REF
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Find or connect ACL connection from DUT.",
      ):
        dut_ref_acl = await self._find_or_connect_acl_from_ref(self.dut.address)

      # Step 4: Trigger connection from DUT and REF at same time
      self.logger.info(
          "[DUT & REF] Triggering simultaneous A2DP (AVDTP) connection."
      )

      # Use asyncio.gather to run both connection attempts concurrently
      try:
        await asyncio.wait_for(
            asyncio.gather(
                self._connect_a2dp_from_dut(self.ref.address),
                self._connect_a2dp_from_ref(dut_ref_acl),
            ),
            timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
        )
      except (core.BaseBumbleError, TimeoutError):
        self.logger.warning(
            "[REF & DUT] Simultaneous A2DP connection exception.",
            stack_info=True,
        )

      # Step 5: Wait for A2DP to be connected on DUT
      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,
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      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,
      )

      # Step 6: Disconnect from DUT
      self.logger.info("Step 6: Disconnect from DUT.")
      self.dut.bt.disconnect(self.ref.address)

      # Step 7: Wait for A2DP to be disconnected on DUT
      self.logger.info("Step 7: Wait for A2DP disconnected.")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.address,
              state=android_constants.ConnectionState.DISCONNECTED,
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

Tests A2DP connection establishment with simultaneous connection.

Test steps
  1. Setup pairing between DUT(A2DP Source) and REF(A2DP Sink).
  2. Terminate ACL connection from DUT.
  3. Setup ACL connection from REF.
  4. Trigger A2DP connection from DUT and REF at same time.
  5. Wait A2DP connected on DUT.
  6. Disconnect from DUT.
  7. Wait A2DP disconnected on DUT.
Test Results

DUT should be able to establish A2DP connection successfully even in conflicting scenarios.

Source code in navi/tests/functionality/a2dp_source_test.py
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
async def test_paired_connect_a2dp_simultaneous(self) -> None:
  """Tests A2DP connection establishment with simultaneous connection.

  Test steps:
    1. Setup pairing between DUT(A2DP Source) and REF(A2DP Sink).
    2. Terminate ACL connection from DUT.
    3. Setup ACL connection from REF.
    4. Trigger A2DP connection from DUT and REF at same time.
    5. Wait A2DP connected on DUT.
    6. Disconnect from DUT.
    7. Wait A2DP disconnected on DUT.

  Test Results:
    DUT should be able to establish A2DP connection successfully even in
    conflicting scenarios.
  """

  with self.dut.bl4a.register_callback(_Module.A2DP) as dut_cb:
    # Step 1: Setup pairing and initial A2DP connection
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[DUT] Setup pairing and initial A2DP connection.",
    ):
      await self._pair_and_connect_from_dut()

    # Step 2: Terminate ACL connection
    await self.disconnect_with_check(
        self.ref.address, android_constants.Transport.CLASSIC
    )

    # Step 3: Setup ACL connection from REF
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Find or connect ACL connection from DUT.",
    ):
      dut_ref_acl = await self._find_or_connect_acl_from_ref(self.dut.address)

    # Step 4: Trigger connection from DUT and REF at same time
    self.logger.info(
        "[DUT & REF] Triggering simultaneous A2DP (AVDTP) connection."
    )

    # Use asyncio.gather to run both connection attempts concurrently
    try:
      await asyncio.wait_for(
          asyncio.gather(
              self._connect_a2dp_from_dut(self.ref.address),
              self._connect_a2dp_from_ref(dut_ref_acl),
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
    except (core.BaseBumbleError, TimeoutError):
      self.logger.warning(
          "[REF & DUT] Simultaneous A2DP connection exception.",
          stack_info=True,
      )

    # Step 5: Wait for A2DP to be connected on DUT
    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,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    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,
    )

    # Step 6: Disconnect from DUT
    self.logger.info("Step 6: Disconnect from DUT.")
    self.dut.bt.disconnect(self.ref.address)

    # Step 7: Wait for A2DP to be disconnected on DUT
    self.logger.info("Step 7: Wait for A2DP disconnected.")
    await dut_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.address,
            state=android_constants.ConnectionState.DISCONNECTED,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Bases: MultiDevicesTestBase

Source code in navi/tests/functionality/asha_dual_devices_test.py
 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
class AshaDualDevicesTest(navi_test_base.MultiDevicesTestBase):
  NUM_REF_DEVICES = 2
  ref_asha_services: list[asha.AshaService] = []

  @override
  async def async_setup_class(self) -> None:
    self.condition = asyncio.Condition()
    await super().async_setup_class()

    if self.dut.getprop(_PROPERTY_ASHA_ENABLED) != 'true':
      raise signals.TestAbortClass('ASHA is not supported on DUT.')

  @override
  async def async_setup_test(self) -> None:
    self.ref_asha_services = list[asha.AshaService]()
    await super().async_setup_test()
    await self._prepare_paired_devices()

    async def on_state_change() -> None:
      async with self.condition:
        self.condition.notify_all()

    watcher = pyee_extensions.EventWatcher()
    self.test_case_context.enter_context(watcher)
    for asha_service in self.ref_asha_services:
      watcher.on(asha_service, asha_service.Event.STARTED, on_state_change)
      watcher.on(asha_service, asha_service.Event.STOPPED, on_state_change)

    self.logger.info('Wait for all ASHA services to be stopped')
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      async with self.condition:
        await self.condition.wait_for(
            lambda: all(
                asha_service.active_codec is None
                for asha_service in self.ref_asha_services
            )
        )

  async def _prepare_paired_devices(self) -> None:
    """Pairs DUT with REF devices."""

    for i, dev in enumerate(self.refs):
      if i == 0:
        device_capabilities = asha.DeviceCapabilities.IS_DUAL
      else:
        device_capabilities = (
            asha.DeviceCapabilities.IS_DUAL | asha.DeviceCapabilities.IS_RIGHT
        )
      asha_service = asha.AshaService(
          capability=asha.DeviceCapabilities(device_capabilities),
          hisyncid=_HISYNC_ID,
          device=dev.device,
      )
      self.ref_asha_services.append(asha_service)
      dev.device.add_service(asha_service)

    with self.dut.bl4a.register_callback(_Module.ASHA) as dut_cb:
      for ref in self.refs:
        await self.le_connect_and_pair(
            ref_address_type=hci.OwnAddressType.RANDOM,
            ref=ref,
            connect_profiles=True,
        )
        self.logger.info(
            '[DUT] Wait for ASHA connected to %s', ref.random_address
        )
        await dut_cb.wait_for_event(
            bl4a_api.ProfileConnectionStateChanged(
                address=ref.random_address,
                state=android_constants.ConnectionState.CONNECTED,
            ),
        )

  async def test_active_devices_should_contain_both_sides(self) -> None:
    """Tests that both sides of the dual device are active."""
    self.assertCountEqual(
        self.dut.bt.getActiveDevices(android_constants.Profile.HEARING_AID),
        [ref.random_address for ref in self.refs],
    )

  @navi_test_base.retry(max_count=3)
  async def test_reconnect(self) -> None:
    """Tests reconnecting ASHA from DUT to REF devices.

    Test steps:
      1. Disconnect ACL from REF devices.
      2. Restart advertising on REF devices.
      3. Wait for DUT to reconnect to REF devices.
    """

    with self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb:
      for ref in self.refs:
        ref_address = ref.random_address
        if not (
            acl := ref.device.find_connection_by_bd_addr(
                hci.Address(self.dut.address),
                transport=core.BT_LE_TRANSPORT,
            )
        ):
          continue
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await acl.disconnect()
        self.logger.info('[DUT] Wait for ACL disconnected from %s', ref_address)
        await dut_cb.wait_for_event(
            bl4a_api.AclDisconnected(
                address=ref_address, transport=android_constants.Transport.LE
            )
        )

    with self.dut.bl4a.register_callback(_Module.ASHA) as dut_cb:
      for ref, asha_service in zip(self.refs, self.ref_asha_services):
        ref_address = ref.random_address
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await ref.device.create_advertising_set(
              advertising_parameters=_DEFAULT_ADVERTISING_PARAMETERS,
              advertising_data=asha_service.get_advertising_data(),
          )
        if ref_address in self.dut.bt.getActiveDevices(
            android_constants.Profile.HEARING_AID
        ):
          self.logger.info('[DUT] ASHA already connected to %s', ref_address)
        else:
          self.logger.info('[DUT] Wait for ASHA connected to %s', ref_address)
          await dut_cb.wait_for_event(
              bl4a_api.ProfileConnectionStateChanged(
                  address=ref_address,
                  state=android_constants.ConnectionState.CONNECTED,
              ),
          )

  @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 usage of stream to test.
    """

    audio_sinks = [asyncio.Queue[bytes](), asyncio.Queue[bytes]()]

    for asha_service, audio_sink in zip(self.ref_asha_services, audio_sinks):
      asha_service.audio_sink = audio_sink.put_nowait

    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.logger.info('[DUT] Set audio attributes.')
      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)
      self.test_case_context.callback(self.dut.bt.audioStop)
      for i in range(len(self.refs)):
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.logger.info('[REF-%d] Wait for audio started', i)
          async with self.condition:
            await self.condition.wait_for(
                lambda: self.ref_asha_services[i].active_codec is not None
            )
          self.logger.info('[REF-%d] Wait for audio data', i)
          await audio_sinks[i].get()

      await asyncio.sleep(_STREAMING_TIME_SECONDS)

      self.logger.info('[DUT] Stop streaming')
      await asyncio.to_thread(self.dut.bt.audioStop)
      for i in range(len(self.refs)):
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.logger.info('[REF-%d] Wait for audio stopped', i)
          async with self.condition:
            await self.condition.wait_for(
                lambda: self.ref_asha_services[i].active_codec is None
            )

  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.
    """
    stream_type = android_constants.StreamType.MUSIC

    volume_lists = [
        pyee_extensions.EventTriggeredValueObserver(
            ref_asha_service,
            asha.AshaService.Event.VOLUME_CHANGED,
            functools.partial(
                lambda service: cast(asha.AshaService, service).volume,
                ref_asha_service,
            ),
        )
        for ref_asha_service in self.ref_asha_services
    ]

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info('[DUT] Set volume to min')
      self.dut.bt.setVolume(stream_type, self.dut.bt.getMinVolume(stream_type))
      for i in range(len(self.refs)):
        self.logger.info('[REF-%d] Wait for volume changed', i)
        await volume_lists[i].wait_for_target_value(-128)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info('[DUT] Set volume to max')
      self.dut.bt.setVolume(stream_type, self.dut.bt.getMaxVolume(stream_type))
      for i in range(len(self.refs)):
        self.logger.info('[REF-%d] Wait for volume changed', i)
        await volume_lists[i].wait_for_target_value(0)

Tests that both sides of the dual device are active.

Source code in navi/tests/functionality/asha_dual_devices_test.py
126
127
128
129
130
131
async def test_active_devices_should_contain_both_sides(self) -> None:
  """Tests that both sides of the dual device are active."""
  self.assertCountEqual(
      self.dut.bt.getActiveDevices(android_constants.Profile.HEARING_AID),
      [ref.random_address for ref in self.refs],
  )

Tests reconnecting ASHA from DUT to REF devices.

Test steps
  1. Disconnect ACL from REF devices.
  2. Restart advertising on REF devices.
  3. Wait for DUT to reconnect to REF devices.
Source code in navi/tests/functionality/asha_dual_devices_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
@navi_test_base.retry(max_count=3)
async def test_reconnect(self) -> None:
  """Tests reconnecting ASHA from DUT to REF devices.

  Test steps:
    1. Disconnect ACL from REF devices.
    2. Restart advertising on REF devices.
    3. Wait for DUT to reconnect to REF devices.
  """

  with self.dut.bl4a.register_callback(_Module.ADAPTER) as dut_cb:
    for ref in self.refs:
      ref_address = ref.random_address
      if not (
          acl := ref.device.find_connection_by_bd_addr(
              hci.Address(self.dut.address),
              transport=core.BT_LE_TRANSPORT,
          )
      ):
        continue
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await acl.disconnect()
      self.logger.info('[DUT] Wait for ACL disconnected from %s', ref_address)
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=ref_address, transport=android_constants.Transport.LE
          )
      )

  with self.dut.bl4a.register_callback(_Module.ASHA) as dut_cb:
    for ref, asha_service in zip(self.refs, self.ref_asha_services):
      ref_address = ref.random_address
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref.device.create_advertising_set(
            advertising_parameters=_DEFAULT_ADVERTISING_PARAMETERS,
            advertising_data=asha_service.get_advertising_data(),
        )
      if ref_address in self.dut.bt.getActiveDevices(
          android_constants.Profile.HEARING_AID
      ):
        self.logger.info('[DUT] ASHA already connected to %s', ref_address)
      else:
        self.logger.info('[DUT] Wait for ASHA connected to %s', ref_address)
        await dut_cb.wait_for_event(
            bl4a_api.ProfileConnectionStateChanged(
                address=ref_address,
                state=android_constants.ConnectionState.CONNECTED,
            ),
        )

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/functionality/asha_dual_devices_test.py
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
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.
  """
  stream_type = android_constants.StreamType.MUSIC

  volume_lists = [
      pyee_extensions.EventTriggeredValueObserver(
          ref_asha_service,
          asha.AshaService.Event.VOLUME_CHANGED,
          functools.partial(
              lambda service: cast(asha.AshaService, service).volume,
              ref_asha_service,
          ),
      )
      for ref_asha_service in self.ref_asha_services
  ]

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info('[DUT] Set volume to min')
    self.dut.bt.setVolume(stream_type, self.dut.bt.getMinVolume(stream_type))
    for i in range(len(self.refs)):
      self.logger.info('[REF-%d] Wait for volume changed', i)
      await volume_lists[i].wait_for_target_value(-128)

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info('[DUT] Set volume to max')
    self.dut.bt.setVolume(stream_type, self.dut.bt.getMaxVolume(stream_type))
    for i in range(len(self.refs)):
      self.logger.info('[REF-%d] Wait for volume changed', i)
      await volume_lists[i].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 usage of stream to test.

required
Source code in navi/tests/functionality/asha_dual_devices_test.py
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
@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 usage of stream to test.
  """

  audio_sinks = [asyncio.Queue[bytes](), asyncio.Queue[bytes]()]

  for asha_service, audio_sink in zip(self.ref_asha_services, audio_sinks):
    asha_service.audio_sink = audio_sink.put_nowait

  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.logger.info('[DUT] Set audio attributes.')
    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)
    self.test_case_context.callback(self.dut.bt.audioStop)
    for i in range(len(self.refs)):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info('[REF-%d] Wait for audio started', i)
        async with self.condition:
          await self.condition.wait_for(
              lambda: self.ref_asha_services[i].active_codec is not None
          )
        self.logger.info('[REF-%d] Wait for audio data', i)
        await audio_sinks[i].get()

    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info('[DUT] Stop streaming')
    await asyncio.to_thread(self.dut.bt.audioStop)
    for i in range(len(self.refs)):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info('[REF-%d] Wait for audio stopped', i)
        async with self.condition:
          await self.condition.wait_for(
              lambda: self.ref_asha_services[i].active_codec is None
          )

Bases: TwoDevicesTestBase

Test Bluetooth Autonomous Repairing.

Source code in navi/tests/functionality/autonomous_repairing_test.py
 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
class AutonomousRepairingTest(navi_test_base.TwoDevicesTestBase):
  """Test Bluetooth Autonomous Repairing."""

  async def _wait_for_repairing_success(
      self,
      ref_address: str,
      adapter_cb: bl4a_api.CallbackHandler,
  ) -> None:
    """Waits for bonding events."""
    self.logger.info("[DUT] Wait for bond state change to none.")
    await adapter_cb.wait_for_event(
        bl4a_api.BondStateChanged(
            address=ref_address,
            state=android_constants.BondState.NONE,
        )
    )

    self.logger.info("[DUT] Wait for bond state change to bonding.")
    await adapter_cb.wait_for_event(
        bl4a_api.BondStateChanged(
            address=ref_address,
            state=android_constants.BondState.BONDING,
        )
    )

    self.logger.info("[DUT] Wait for encryption changed.")
    await adapter_cb.wait_for_event(
        bl4a_api.EncryptionChanged(address=ref_address)
    )

    self.logger.info("[DUT] Wait for bond state change to bonded.")
    await adapter_cb.wait_for_event(
        bl4a_api.BondStateChanged(
            address=ref_address,
            state=android_constants.BondState.BONDED,
        )
    )

    if not self.ref.device.keystore:
      self.fail("[REF] Keystore is not initialized.")

    self.assertIsNotNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

  async def _wait_for_repairing_fail(
      self,
      ref_address: str,
      adapter_cb: bl4a_api.CallbackHandler,
      transport: android_constants.Transport,
  ) -> None:
    """Waits for repairing fail events."""
    self.logger.info("[DUT] Wait for ACL disconnection.")
    await adapter_cb.wait_for_event(
        bl4a_api.AclDisconnected(
            address=ref_address,
            transport=transport,
        ),
        timeout=_DEFAULT_ACL_DISCONNECTION_TIMEOUT_SECONDS,
    )

    self.logger.info("[DUT] Wait for key missing.")
    await adapter_cb.wait_for_event(
        bl4a_api.KeyMissing(
            address=ref_address,
        )
    )

    if not self.ref.device.keystore:
      self.fail("[REF] Keystore is not initialized.")

    self.assertIsNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

  # TODO: Remove this skip once the bug is fixed.
  @navi_test_base.TwoDevicesTestBase.require_flag(
      "com.android.bluetooth.flags.autonomous_repairing_initiation",
      "android.bluetooth.platform.flags.autonomous_repairing_initiation",
  )
  @navi_test_base.parameterized(
      *itertools.product(
          [
              TestVariant.ACCEPT,
              TestVariant.REJECTED,
              TestVariant.DISCONNECTED,
              TestVariant.NOT_RESPONDED,
          ],
          [constants.Direction.OUTGOING, constants.Direction.INCOMING],
      )
  )
  async def test_repairing_classic(
      self,
      variant: TestVariant,
      pairing_direction: constants.Direction,
  ) -> None:
    """Tests re-pairing when the remote device loses the bond over BR/EDR.

    Test steps:
      1. Bond DUT and REF over BR/EDR.
      2. Disconnect from DUT.
      3. Remove the bond on REF.
      4. Initiate connection depending on pairing_direction.
      5. Verify DUT detects bond loss and initiates re-pairing.
      6. Verify DUT bond with REF.
      7. Accept or reject pairing requests on REF.
      8. [If accepted] Verify REF has the key for DUT.
      9. Verify DUT has the key for REF.

    Args:
      variant: Whether to accept or reject the pairing request on REF.
      pairing_direction: The direction of the pairing request.
    """
    self.logger.info("[REF] Setup A2DP record.")
    self.ref.device.sdp_service_records = {
        1: a2dp.make_audio_sink_service_sdp_records(1),
    }

    await self.classic_connect_and_pair()

    await self.disconnect_with_check(
        self.ref.address, android_constants.Transport.CLASSIC
    )

    pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=_IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        auto_accept=True,
    )

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

    self.logger.info("[REF] Set pairing config factory.")
    self.ref.device.pairing_config_factory = pairing_config_factory

    if not self.ref.device.keystore:
      self.fail("[REF] Keystore is not initialized.")

    self.assertIsNotNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

    self.logger.info("[REF] Delete all keys.")
    await self.ref.device.keystore.delete_all()

    self.logger.info("[REF] Clear resolving list in the controller.")
    await self.ref.device.send_command(
        hci.HCI_LE_Clear_Resolving_List_Command()
    )

    self.assertIsNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

    adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(adapter_cb)

    auth_task: asyncio.tasks.Task | None = None
    ref_dut_acl: device.Connection | None

    if pairing_direction == constants.Direction.OUTGOING:
      self.logger.info("[DUT] Initiate ACL connection from DUT.")
      self.dut.bt.connect(self.ref.address)
    else:
      self.logger.info("[REF] Connect to DUT.")
      ref_dut_acl = 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_acl.authenticate())

    self.logger.info("[DUT] Wait for connection.")
    await adapter_cb.wait_for_event(
        event=bl4a_api.AclConnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        ),
    )

    self.logger.info("[DUT] Wait for pairing request.")
    await adapter_cb.wait_for_event(
        bl4a_api.PairingRequest(
            address=self.ref.address, variant=mock.ANY, pin=mock.ANY
        )
    )

    self.logger.info("[DUT] Get bonded devices.")
    self.assertIn(self.ref.address, self.dut.bt.getBondedDevices())

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

    self.logger.info("[DUT] Accept pairing request.")
    self.dut.bt.setPairingConfirmation(self.ref.address, True)

    ref_accept = variant == TestVariant.ACCEPT

    match variant:
      case TestVariant.ACCEPT:
        self.logger.info("[REF] Accept pairing request.")
        pairing_delegate.pairing_answers.put_nowait(True)

      case TestVariant.REJECTED:
        self.logger.info("[REF] Reject pairing request.")
        pairing_delegate.pairing_answers.put_nowait(False)

      case TestVariant.NOT_RESPONDED:
        self.logger.info("[REF] No response.")

      case TestVariant.DISCONNECTED:
        ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
            hci.Address(self.dut.address),
            transport=core.PhysicalTransport.BR_EDR,
        )
        if not ref_dut_acl:
          self.fail("[REF] No ACL connection found.")

        self.logger.info("[REF] Disconnect from DUT.")
        await ref_dut_acl.disconnect()

    if ref_accept:
      await self._wait_for_repairing_success(
          ref_address=self.ref.address,
          adapter_cb=adapter_cb,
      )
    else:
      await self._wait_for_repairing_fail(
          ref_address=self.ref.address,
          adapter_cb=adapter_cb,
          transport=android_constants.Transport.CLASSIC,
      )

    self.assertIn(self.ref.address, self.dut.bt.getBondedDevices())

    if auth_task:
      self.logger.info("[REF] Wait for 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

  # # TODO: Remove this skip once the bug is fixed.
  @navi_test_base.TwoDevicesTestBase.require_flag(
      "com.android.bluetooth.flags.autonomous_repairing_initiation",
      "android.bluetooth.platform.flags.autonomous_repairing_initiation",
  )
  @navi_test_base.parameterized(
      *itertools.product(
          [
              TestVariant.ACCEPT,
              TestVariant.REJECTED,
              TestVariant.DISCONNECTED,
              TestVariant.NOT_RESPONDED,
          ],
          [constants.Direction.OUTGOING, constants.Direction.INCOMING],
      )
  )
  async def test_repairing_le(
      self,
      variant: TestVariant,
      pairing_direction: constants.Direction,
  ) -> None:
    """Tests re-pairing when the remote device loses the bond over LE.

    Test steps:
      1. Bond DUT and REF over LE.
      2. Disconnect from DUT.
      3. Remove the bond on REF.
      4. Initiate connection depending on pairing_direction.
      5. Verify DUT detects bond loss and initiates re-pairing.
      6. Verify DUT bond with REF.
      7. Accept or reject pairing requests on REF.
      8. [If accepted] Verify REF has the key for DUT.
      9. Verify DUT has the key for REF.

    Args:
      variant: Whether to accept or reject the pairing request on REF.
      pairing_direction: The direction of the pairing request.
    """
    service_uuid = str(uuid.uuid4())

    self.logger.info("[REF] Add GATT service with UUID: %s", service_uuid)
    self.ref.device.add_service(
        gatt.Service(uuid=service_uuid, characteristics=[])
    )

    await self.le_connect_and_pair(
        ref_address_type=hci.OwnAddressType.RANDOM,
    )

    await self.disconnect_with_check(
        self.ref.random_address, android_constants.Transport.LE
    )

    pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=_IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        auto_accept=True,
    )

    def pairing_config_factory(
        _: device.Connection,
    ) -> pairing.PairingConfig:
      return pairing.PairingConfig(
          identity_address_type=pairing.PairingConfig.AddressType.RANDOM,
          delegate=pairing_delegate,
      )

    self.logger.info("[REF] Set pairing config factory.")
    self.ref.device.pairing_config_factory = pairing_config_factory

    if not self.ref.device.keystore:
      self.fail("[REF] Keystore is not initialized.")

    self.assertIsNotNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

    self.logger.info("[REF] Delete all keys.")
    await self.ref.device.keystore.delete_all()

    self.logger.info("[REF] Clear resolving list in the controller.")
    await self.ref.device.send_command(
        hci.HCI_LE_Clear_Resolving_List_Command()
    )

    self.assertIsNone(
        await self.ref.device.keystore.get(f"{self.dut.address}/P")
    )

    adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(adapter_cb)

    pair_task: asyncio.tasks.Task | None = None

    if pairing_direction == constants.Direction.OUTGOING:
      self.logger.info("[REF] Start advertising")
      await self.ref.device.create_advertising_set(
          advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
      )

      self.logger.info("[DUT] Initiate ACL connection from DUT.")
      gatt_client = await self.dut.bl4a.connect_gatt_client(
          address=self.ref.random_address,
          transport=android_constants.Transport.LE,
      )
      self.test_case_context.push(gatt_client)
    else:
      self.logger.info("[DUT] Start advertising.")
      advertise = await self.dut.bl4a.start_legacy_advertiser(
          settings=bl4a_api.LegacyAdvertiseSettings(
              own_address_type=android_constants.AddressTypeStatus.RANDOM
          ),
          advertising_data=bl4a_api.AdvertisingData(
              service_uuids=[service_uuid]
          ),
      )

      self.logger.info("[REF] Scan for DUT.")
      scan_result = asyncio.get_running_loop().create_future()
      with advertise, pyee_extensions.EventWatcher() as watcher:

        def on_advertising_report(adv: device.Advertisement) -> None:
          if service_uuids := adv.data.get(
              core.AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
          ):
            if service_uuid in service_uuids and not scan_result.done():
              scan_result.set_result(adv.address)

        watcher.on(self.ref.device, "advertisement", on_advertising_report)

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

        self.logger.info("[REF] Wait for advertising report from DUT.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          dut_addr = await scan_result

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

        ref_dut_acl: device.Connection | None
        self.logger.info("[REF] Connect to DUT.")
        ref_dut_acl = await self.ref.device.connect(
            dut_addr,
            transport=core.BT_LE_TRANSPORT,
            own_address_type=hci.OwnAddressType.RANDOM,
            timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
        )

        self.logger.info("[REF] Get remote LE features.")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await ref_dut_acl.get_remote_le_features()

        self.logger.info("[REF] Pair.")
        pair_task = asyncio.create_task(ref_dut_acl.pair())

    self.logger.info("[DUT] Wait for connection.")
    await adapter_cb.wait_for_event(
        event=bl4a_api.AclConnected(
            address=self.ref.random_address,
            transport=android_constants.Transport.LE,
        ),
    )

    self.logger.info("[DUT] Wait for pairing request.")
    await adapter_cb.wait_for_event(
        bl4a_api.PairingRequest(
            address=self.ref.random_address, variant=mock.ANY, pin=mock.ANY
        )
    )

    self.logger.info("[DUT] Get bonded devices.")
    self.assertIn(self.ref.random_address, self.dut.bt.getBondedDevices())

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

    self.logger.info("[DUT] Accept pairing request.")
    self.dut.bt.setPairingConfirmation(self.ref.random_address, True)

    ref_accept = variant == TestVariant.ACCEPT

    match variant:
      case TestVariant.ACCEPT:
        self.logger.info("[REF] Accept pairing request.")
        pairing_delegate.pairing_answers.put_nowait(True)

      case TestVariant.REJECTED:
        self.logger.info("[REF] Reject pairing request.")
        pairing_delegate.pairing_answers.put_nowait(False)

      case TestVariant.NOT_RESPONDED:
        self.logger.info("[REF] No response.")

      case TestVariant.DISCONNECTED:
        ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
            hci.Address(self.dut.address),
            transport=core.PhysicalTransport.LE,
        )
        if not ref_dut_acl:
          self.fail("[REF] No ACL connection found.")

        self.logger.info("[REF] Disconnect from DUT.")
        await ref_dut_acl.disconnect()

    if ref_accept:
      await self._wait_for_repairing_success(
          ref_address=self.ref.random_address,
          adapter_cb=adapter_cb,
      )
    else:
      await self._wait_for_repairing_fail(
          ref_address=self.ref.random_address,
          adapter_cb=adapter_cb,
          transport=android_constants.Transport.LE,
      )

    self.assertIn(self.ref.random_address, self.dut.bt.getBondedDevices())

    if pair_task:
      self.logger.info("[REF] Wait pairing complete.")
      if variant == TestVariant.ACCEPT:
        await pair_task
      else:
        with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
          await pair_task

Tests re-pairing when the remote device loses the bond over BR/EDR.

Test steps
  1. Bond DUT and REF over BR/EDR.
  2. Disconnect from DUT.
  3. Remove the bond on REF.
  4. Initiate connection depending on pairing_direction.
  5. Verify DUT detects bond loss and initiates re-pairing.
  6. Verify DUT bond with REF.
  7. Accept or reject pairing requests on REF.
  8. [If accepted] Verify REF has the key for DUT.
  9. Verify DUT has the key for REF.

Parameters:

Name Type Description Default
variant TestVariant

Whether to accept or reject the pairing request on REF.

required
pairing_direction Direction

The direction of the pairing request.

required
Source code in navi/tests/functionality/autonomous_repairing_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
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
@navi_test_base.TwoDevicesTestBase.require_flag(
    "com.android.bluetooth.flags.autonomous_repairing_initiation",
    "android.bluetooth.platform.flags.autonomous_repairing_initiation",
)
@navi_test_base.parameterized(
    *itertools.product(
        [
            TestVariant.ACCEPT,
            TestVariant.REJECTED,
            TestVariant.DISCONNECTED,
            TestVariant.NOT_RESPONDED,
        ],
        [constants.Direction.OUTGOING, constants.Direction.INCOMING],
    )
)
async def test_repairing_classic(
    self,
    variant: TestVariant,
    pairing_direction: constants.Direction,
) -> None:
  """Tests re-pairing when the remote device loses the bond over BR/EDR.

  Test steps:
    1. Bond DUT and REF over BR/EDR.
    2. Disconnect from DUT.
    3. Remove the bond on REF.
    4. Initiate connection depending on pairing_direction.
    5. Verify DUT detects bond loss and initiates re-pairing.
    6. Verify DUT bond with REF.
    7. Accept or reject pairing requests on REF.
    8. [If accepted] Verify REF has the key for DUT.
    9. Verify DUT has the key for REF.

  Args:
    variant: Whether to accept or reject the pairing request on REF.
    pairing_direction: The direction of the pairing request.
  """
  self.logger.info("[REF] Setup A2DP record.")
  self.ref.device.sdp_service_records = {
      1: a2dp.make_audio_sink_service_sdp_records(1),
  }

  await self.classic_connect_and_pair()

  await self.disconnect_with_check(
      self.ref.address, android_constants.Transport.CLASSIC
  )

  pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=_IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
      auto_accept=True,
  )

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

  self.logger.info("[REF] Set pairing config factory.")
  self.ref.device.pairing_config_factory = pairing_config_factory

  if not self.ref.device.keystore:
    self.fail("[REF] Keystore is not initialized.")

  self.assertIsNotNone(
      await self.ref.device.keystore.get(f"{self.dut.address}/P")
  )

  self.logger.info("[REF] Delete all keys.")
  await self.ref.device.keystore.delete_all()

  self.logger.info("[REF] Clear resolving list in the controller.")
  await self.ref.device.send_command(
      hci.HCI_LE_Clear_Resolving_List_Command()
  )

  self.assertIsNone(
      await self.ref.device.keystore.get(f"{self.dut.address}/P")
  )

  adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(adapter_cb)

  auth_task: asyncio.tasks.Task | None = None
  ref_dut_acl: device.Connection | None

  if pairing_direction == constants.Direction.OUTGOING:
    self.logger.info("[DUT] Initiate ACL connection from DUT.")
    self.dut.bt.connect(self.ref.address)
  else:
    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = 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_acl.authenticate())

  self.logger.info("[DUT] Wait for connection.")
  await adapter_cb.wait_for_event(
      event=bl4a_api.AclConnected(
          address=self.ref.address,
          transport=android_constants.Transport.CLASSIC,
      ),
  )

  self.logger.info("[DUT] Wait for pairing request.")
  await adapter_cb.wait_for_event(
      bl4a_api.PairingRequest(
          address=self.ref.address, variant=mock.ANY, pin=mock.ANY
      )
  )

  self.logger.info("[DUT] Get bonded devices.")
  self.assertIn(self.ref.address, self.dut.bt.getBondedDevices())

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

  self.logger.info("[DUT] Accept pairing request.")
  self.dut.bt.setPairingConfirmation(self.ref.address, True)

  ref_accept = variant == TestVariant.ACCEPT

  match variant:
    case TestVariant.ACCEPT:
      self.logger.info("[REF] Accept pairing request.")
      pairing_delegate.pairing_answers.put_nowait(True)

    case TestVariant.REJECTED:
      self.logger.info("[REF] Reject pairing request.")
      pairing_delegate.pairing_answers.put_nowait(False)

    case TestVariant.NOT_RESPONDED:
      self.logger.info("[REF] No response.")

    case TestVariant.DISCONNECTED:
      ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          transport=core.PhysicalTransport.BR_EDR,
      )
      if not ref_dut_acl:
        self.fail("[REF] No ACL connection found.")

      self.logger.info("[REF] Disconnect from DUT.")
      await ref_dut_acl.disconnect()

  if ref_accept:
    await self._wait_for_repairing_success(
        ref_address=self.ref.address,
        adapter_cb=adapter_cb,
    )
  else:
    await self._wait_for_repairing_fail(
        ref_address=self.ref.address,
        adapter_cb=adapter_cb,
        transport=android_constants.Transport.CLASSIC,
    )

  self.assertIn(self.ref.address, self.dut.bt.getBondedDevices())

  if auth_task:
    self.logger.info("[REF] Wait for 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

Tests re-pairing when the remote device loses the bond over LE.

Test steps
  1. Bond DUT and REF over LE.
  2. Disconnect from DUT.
  3. Remove the bond on REF.
  4. Initiate connection depending on pairing_direction.
  5. Verify DUT detects bond loss and initiates re-pairing.
  6. Verify DUT bond with REF.
  7. Accept or reject pairing requests on REF.
  8. [If accepted] Verify REF has the key for DUT.
  9. Verify DUT has the key for REF.

Parameters:

Name Type Description Default
variant TestVariant

Whether to accept or reject the pairing request on REF.

required
pairing_direction Direction

The direction of the pairing request.

required
Source code in navi/tests/functionality/autonomous_repairing_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
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
@navi_test_base.TwoDevicesTestBase.require_flag(
    "com.android.bluetooth.flags.autonomous_repairing_initiation",
    "android.bluetooth.platform.flags.autonomous_repairing_initiation",
)
@navi_test_base.parameterized(
    *itertools.product(
        [
            TestVariant.ACCEPT,
            TestVariant.REJECTED,
            TestVariant.DISCONNECTED,
            TestVariant.NOT_RESPONDED,
        ],
        [constants.Direction.OUTGOING, constants.Direction.INCOMING],
    )
)
async def test_repairing_le(
    self,
    variant: TestVariant,
    pairing_direction: constants.Direction,
) -> None:
  """Tests re-pairing when the remote device loses the bond over LE.

  Test steps:
    1. Bond DUT and REF over LE.
    2. Disconnect from DUT.
    3. Remove the bond on REF.
    4. Initiate connection depending on pairing_direction.
    5. Verify DUT detects bond loss and initiates re-pairing.
    6. Verify DUT bond with REF.
    7. Accept or reject pairing requests on REF.
    8. [If accepted] Verify REF has the key for DUT.
    9. Verify DUT has the key for REF.

  Args:
    variant: Whether to accept or reject the pairing request on REF.
    pairing_direction: The direction of the pairing request.
  """
  service_uuid = str(uuid.uuid4())

  self.logger.info("[REF] Add GATT service with UUID: %s", service_uuid)
  self.ref.device.add_service(
      gatt.Service(uuid=service_uuid, characteristics=[])
  )

  await self.le_connect_and_pair(
      ref_address_type=hci.OwnAddressType.RANDOM,
  )

  await self.disconnect_with_check(
      self.ref.random_address, android_constants.Transport.LE
  )

  pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=_IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
      auto_accept=True,
  )

  def pairing_config_factory(
      _: device.Connection,
  ) -> pairing.PairingConfig:
    return pairing.PairingConfig(
        identity_address_type=pairing.PairingConfig.AddressType.RANDOM,
        delegate=pairing_delegate,
    )

  self.logger.info("[REF] Set pairing config factory.")
  self.ref.device.pairing_config_factory = pairing_config_factory

  if not self.ref.device.keystore:
    self.fail("[REF] Keystore is not initialized.")

  self.assertIsNotNone(
      await self.ref.device.keystore.get(f"{self.dut.address}/P")
  )

  self.logger.info("[REF] Delete all keys.")
  await self.ref.device.keystore.delete_all()

  self.logger.info("[REF] Clear resolving list in the controller.")
  await self.ref.device.send_command(
      hci.HCI_LE_Clear_Resolving_List_Command()
  )

  self.assertIsNone(
      await self.ref.device.keystore.get(f"{self.dut.address}/P")
  )

  adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(adapter_cb)

  pair_task: asyncio.tasks.Task | None = None

  if pairing_direction == constants.Direction.OUTGOING:
    self.logger.info("[REF] Start advertising")
    await self.ref.device.create_advertising_set(
        advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
    )

    self.logger.info("[DUT] Initiate ACL connection from DUT.")
    gatt_client = await self.dut.bl4a.connect_gatt_client(
        address=self.ref.random_address,
        transport=android_constants.Transport.LE,
    )
    self.test_case_context.push(gatt_client)
  else:
    self.logger.info("[DUT] Start advertising.")
    advertise = await self.dut.bl4a.start_legacy_advertiser(
        settings=bl4a_api.LegacyAdvertiseSettings(
            own_address_type=android_constants.AddressTypeStatus.RANDOM
        ),
        advertising_data=bl4a_api.AdvertisingData(
            service_uuids=[service_uuid]
        ),
    )

    self.logger.info("[REF] Scan for DUT.")
    scan_result = asyncio.get_running_loop().create_future()
    with advertise, pyee_extensions.EventWatcher() as watcher:

      def on_advertising_report(adv: device.Advertisement) -> None:
        if service_uuids := adv.data.get(
            core.AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
        ):
          if service_uuid in service_uuids and not scan_result.done():
            scan_result.set_result(adv.address)

      watcher.on(self.ref.device, "advertisement", on_advertising_report)

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

      self.logger.info("[REF] Wait for advertising report from DUT.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        dut_addr = await scan_result

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

      ref_dut_acl: device.Connection | None
      self.logger.info("[REF] Connect to DUT.")
      ref_dut_acl = await self.ref.device.connect(
          dut_addr,
          transport=core.BT_LE_TRANSPORT,
          own_address_type=hci.OwnAddressType.RANDOM,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

      self.logger.info("[REF] Get remote LE features.")
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref_dut_acl.get_remote_le_features()

      self.logger.info("[REF] Pair.")
      pair_task = asyncio.create_task(ref_dut_acl.pair())

  self.logger.info("[DUT] Wait for connection.")
  await adapter_cb.wait_for_event(
      event=bl4a_api.AclConnected(
          address=self.ref.random_address,
          transport=android_constants.Transport.LE,
      ),
  )

  self.logger.info("[DUT] Wait for pairing request.")
  await adapter_cb.wait_for_event(
      bl4a_api.PairingRequest(
          address=self.ref.random_address, variant=mock.ANY, pin=mock.ANY
      )
  )

  self.logger.info("[DUT] Get bonded devices.")
  self.assertIn(self.ref.random_address, self.dut.bt.getBondedDevices())

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

  self.logger.info("[DUT] Accept pairing request.")
  self.dut.bt.setPairingConfirmation(self.ref.random_address, True)

  ref_accept = variant == TestVariant.ACCEPT

  match variant:
    case TestVariant.ACCEPT:
      self.logger.info("[REF] Accept pairing request.")
      pairing_delegate.pairing_answers.put_nowait(True)

    case TestVariant.REJECTED:
      self.logger.info("[REF] Reject pairing request.")
      pairing_delegate.pairing_answers.put_nowait(False)

    case TestVariant.NOT_RESPONDED:
      self.logger.info("[REF] No response.")

    case TestVariant.DISCONNECTED:
      ref_dut_acl = self.ref.device.find_connection_by_bd_addr(
          hci.Address(self.dut.address),
          transport=core.PhysicalTransport.LE,
      )
      if not ref_dut_acl:
        self.fail("[REF] No ACL connection found.")

      self.logger.info("[REF] Disconnect from DUT.")
      await ref_dut_acl.disconnect()

  if ref_accept:
    await self._wait_for_repairing_success(
        ref_address=self.ref.random_address,
        adapter_cb=adapter_cb,
    )
  else:
    await self._wait_for_repairing_fail(
        ref_address=self.ref.random_address,
        adapter_cb=adapter_cb,
        transport=android_constants.Transport.LE,
    )

  self.assertIn(self.ref.random_address, self.dut.bt.getBondedDevices())

  if pair_task:
    self.logger.info("[REF] Wait pairing complete.")
    if variant == TestVariant.ACCEPT:
      await pair_task
    else:
      with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
        await pair_task

Bases: TwoDevicesTestBase

Tests Bluetooth Quality Report.

Source code in navi/tests/functionality/bqr_test.py
 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 BluetoothQualityReportTest(navi_test_base.TwoDevicesTestBase):
  """Tests Bluetooth Quality Report."""
  bqr_event_mask: int

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    bqr_event_mask_str = self.dut.getprop("persist.bluetooth.bqr.event_mask")
    if not bqr_event_mask_str:
      raise signals.TestAbortClass("BQR is not enabled on DUT.")
    self.bqr_event_mask = int(bqr_event_mask_str)

  async def test_approach_lsto_classic_connection(self) -> None:
    """Tests classic connection approach LSTO."""

    bqr_cb = self.dut.bl4a.register_callback(bl4a_api.Module.BQR)
    adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(bqr_cb)
    self.test_case_context.push(adapter_cb)

    self.logger.info("[REF] Connect to DUT.")
    ref_dut_acl = await self.ref.device.connect(
        self.dut.address,
        transport=core.PhysicalTransport.BR_EDR,
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    await adapter_cb.wait_for_event(
        bl4a_api.AclConnected(
            address=self.ref.address,
            transport=android_constants.Transport.CLASSIC,
        )
    )

    self.logger.info("[REF] Disconnect to trigger LSTO.")
    # Reason must be power off, or else LSTO will not be triggered.
    await ref_dut_acl.disconnect(
        reason=hci.HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF_ERROR
    )

    if self.bqr_event_mask & BqrEventMaskBitIndex.APPROACH_LSTO:
      self.logger.info("[DUT] Wait for BQR event: APPROACH_LSTO.")
      await bqr_cb.wait_for_event(
          bl4a_api.BluetoothQualityReportReady(
              device=self.ref.address,
              quality_report_id=android_constants.BluetoothQualityReportId.APPROACH_LSTO,
              status=0,
              common=mock.ANY,
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
    if self.bqr_event_mask & BqrEventMaskBitIndex.RF_STATS_MODE_EVENT_TRIGGER:
      self.logger.info("[DUT] Wait for BQR event: RF_STATS.")
      await bqr_cb.wait_for_event(
          bl4a_api.BluetoothQualityReportReady(
              device=_MASKED_ADDRESS,
              quality_report_id=android_constants.BluetoothQualityReportId.RF_STATS,
              status=0,
              common=mock.ANY,
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  async def test_energy_monitoring_when_power_unplug(self) -> None:
    """Tests power unplug will trigger energy monitoring."""

    if self.dut.bt.getSdkVersion() < 36:
      self.skipTest(
          "Energy monitor event is not supported before SDK API level: 36."
      )
    if not self.bqr_event_mask & BqrEventMaskBitIndex.ENERGY_MONITORING_MODE:
      self.skipTest("Energy monitor event is not enabled on DUT.")

    bqr_cb = self.dut.bl4a.register_callback(bl4a_api.Module.BQR)
    self.test_case_context.push(bqr_cb)

    self.logger.info("[DUT] Set battery unplug and battery level to low.")
    self.dut.shell("cmd battery unplug")
    self.dut.shell("cmd battery set level 9")
    self.test_case_context.callback(lambda: self.dut.shell("cmd battery reset"))

    self.logger.info("[DUT] Wait for BQR event.")
    await bqr_cb.wait_for_event(
        bl4a_api.BluetoothQualityReportReady(
            device=_MASKED_ADDRESS,
            quality_report_id=android_constants.BluetoothQualityReportId.ENERGY_MONITOR,
            status=0,
            common=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Tests classic connection approach LSTO.

Source code in navi/tests/functionality/bqr_test.py
 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
async def test_approach_lsto_classic_connection(self) -> None:
  """Tests classic connection approach LSTO."""

  bqr_cb = self.dut.bl4a.register_callback(bl4a_api.Module.BQR)
  adapter_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(bqr_cb)
  self.test_case_context.push(adapter_cb)

  self.logger.info("[REF] Connect to DUT.")
  ref_dut_acl = await self.ref.device.connect(
      self.dut.address,
      transport=core.PhysicalTransport.BR_EDR,
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  await adapter_cb.wait_for_event(
      bl4a_api.AclConnected(
          address=self.ref.address,
          transport=android_constants.Transport.CLASSIC,
      )
  )

  self.logger.info("[REF] Disconnect to trigger LSTO.")
  # Reason must be power off, or else LSTO will not be triggered.
  await ref_dut_acl.disconnect(
      reason=hci.HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF_ERROR
  )

  if self.bqr_event_mask & BqrEventMaskBitIndex.APPROACH_LSTO:
    self.logger.info("[DUT] Wait for BQR event: APPROACH_LSTO.")
    await bqr_cb.wait_for_event(
        bl4a_api.BluetoothQualityReportReady(
            device=self.ref.address,
            quality_report_id=android_constants.BluetoothQualityReportId.APPROACH_LSTO,
            status=0,
            common=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
  if self.bqr_event_mask & BqrEventMaskBitIndex.RF_STATS_MODE_EVENT_TRIGGER:
    self.logger.info("[DUT] Wait for BQR event: RF_STATS.")
    await bqr_cb.wait_for_event(
        bl4a_api.BluetoothQualityReportReady(
            device=_MASKED_ADDRESS,
            quality_report_id=android_constants.BluetoothQualityReportId.RF_STATS,
            status=0,
            common=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )

Tests power unplug will trigger energy monitoring.

Source code in navi/tests/functionality/bqr_test.py
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_energy_monitoring_when_power_unplug(self) -> None:
  """Tests power unplug will trigger energy monitoring."""

  if self.dut.bt.getSdkVersion() < 36:
    self.skipTest(
        "Energy monitor event is not supported before SDK API level: 36."
    )
  if not self.bqr_event_mask & BqrEventMaskBitIndex.ENERGY_MONITORING_MODE:
    self.skipTest("Energy monitor event is not enabled on DUT.")

  bqr_cb = self.dut.bl4a.register_callback(bl4a_api.Module.BQR)
  self.test_case_context.push(bqr_cb)

  self.logger.info("[DUT] Set battery unplug and battery level to low.")
  self.dut.shell("cmd battery unplug")
  self.dut.shell("cmd battery set level 9")
  self.test_case_context.callback(lambda: self.dut.shell("cmd battery reset"))

  self.logger.info("[DUT] Wait for BQR event.")
  await bqr_cb.wait_for_event(
      bl4a_api.BluetoothQualityReportReady(
          device=_MASKED_ADDRESS,
          quality_report_id=android_constants.BluetoothQualityReportId.ENERGY_MONITOR,
          status=0,
          common=mock.ANY,
      ),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )

Bases: TwoDevicesTestBase

Tests related to Bluetooth Classic pairing.

Source code in navi/tests/functionality/classic_pairing_test.py
 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
class ClassicPairingTest(navi_test_base.TwoDevicesTestBase):
  """Tests related to Bluetooth Classic pairing."""

  async def test_legacy_pairing_incoming_mode3(self) -> None:
    """Tests incoming Legacy Pairing in mode 3.

    Test steps:
      1. Enable always Authentication and disable build-in pairing delegation on
      REF.
      2. Connect 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.
    """

    pairing_delegate = pairing_utils.PairingDelegate(
        io_capability=pairing.PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY,
        auto_accept=False,
    )

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

    self.ref.device.pairing_config_factory = pairing_config_factory

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    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),
        check_result=True,
    )
    self.logger.info('[REF] Enable always authenticate on REF.')
    await self.ref.device.send_command(
        hci.HCI_Write_Authentication_Enable_Command(authentication_enable=1),
        check_result=True,
    )

    self.logger.info('[REF] Connect to DUT.')
    ref_connection_task = asyncio.create_task(
        self.ref.device.connect(
            self.dut.address,
            transport=core.BT_BR_EDR_TRANSPORT,
        )
    )

    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,
          pairing_utils.PairingVariant.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(
        bl4a_api.PairingRequest,
        predicate=lambda e: (e.address == self.ref.address),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.assertEqual(
        dut_pairing_request.variant, android_constants.PairingVariant.PIN
    )

    self.logger.info('[DUT] Handle pairing confirmation.')
    self.dut.bt.setPin(self.ref.address, _PIN_CODE_DEFAULT)

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

    self.logger.info('[REF] Wait for connection complete.')
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_connection_task

Tests incoming Legacy Pairing in mode 3.

Test steps
  1. Enable always Authentication and disable build-in pairing delegation on REF.
  2. Connect 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/functionality/classic_pairing_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
async def test_legacy_pairing_incoming_mode3(self) -> None:
  """Tests incoming Legacy Pairing in mode 3.

  Test steps:
    1. Enable always Authentication and disable build-in pairing delegation on
    REF.
    2. Connect 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.
  """

  pairing_delegate = pairing_utils.PairingDelegate(
      io_capability=pairing.PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY,
      auto_accept=False,
  )

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

  self.ref.device.pairing_config_factory = pairing_config_factory

  dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(dut_cb)
  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),
      check_result=True,
  )
  self.logger.info('[REF] Enable always authenticate on REF.')
  await self.ref.device.send_command(
      hci.HCI_Write_Authentication_Enable_Command(authentication_enable=1),
      check_result=True,
  )

  self.logger.info('[REF] Connect to DUT.')
  ref_connection_task = asyncio.create_task(
      self.ref.device.connect(
          self.dut.address,
          transport=core.BT_BR_EDR_TRANSPORT,
      )
  )

  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,
        pairing_utils.PairingVariant.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(
      bl4a_api.PairingRequest,
      predicate=lambda e: (e.address == self.ref.address),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  self.assertEqual(
      dut_pairing_request.variant, android_constants.PairingVariant.PIN
  )

  self.logger.info('[DUT] Handle pairing confirmation.')
  self.dut.bt.setPin(self.ref.address, _PIN_CODE_DEFAULT)

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

  self.logger.info('[REF] Wait for connection complete.')
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await ref_connection_task

Bases: MultiDevicesTestBase

Source code in navi/tests/functionality/coex_test.py
  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
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
class CoexTest(navi_test_base.MultiDevicesTestBase):

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    for i, ref in enumerate(self.refs):
      self.logger.info(
          "[REF-%d] Disable CTKD over Classic to avoid blocking SDP.", i
      )
      ref.config.classic_smp_enabled = False

    if self.dut.device.is_emulator:
      self.setprop_for_class_context(
          android_constants.Property.HFP_HF_ENABLED, "true"
      )

      self.setprop_for_class_context(_PROPERTY_HF_FEATURES, "0x1b5")

    if (
        self.dut.getprop(_AndroidProperty.BAP_UNICAST_CLIENT_ENABLED) == "true"
        and self.dut.bt.getHardware() != "cutf_cvm"
    ):
      self.setprop_for_class_context(
          _AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST, "true"
      )

  @override
  async def async_teardown_test(self) -> None:
    self.logger.info("[DUT] Stop audio.")
    self.dut.bt.audioStop()

    self.logger.info("[DUT] Reset audio attributes to default.")
    self.dut.bt.setAudioAttributes(None, False)

    await super().async_teardown_test()

  def _setup_headset_device(
      self,
      hf_configuration: hfp.HfConfiguration,
      a2dp_codecs: collections.abc.Sequence[a2dp_ext.A2dpCodec],
  ) -> None:
    """Setup HF and A2DP services on the REF device."""
    for i, ref in enumerate(self.refs):
      self.logger.info("[REF-%d] Setup HFP HF.", i)
      hfp_ext.HfProtocol.setup_server(
          ref.device,
          sdp_handle=_HFP_HF_SDP_HANDLE,
          configuration=hf_configuration,
      )

      self.logger.info("[REF-%d] Setup A2DP sink.", i)
      a2dp_ext.setup_sink_server(
          ref.device,
          [codec.get_default_capabilities() for codec in a2dp_codecs],
          _A2DP_SERVICE_RECORD_HANDLE,
      )

      self.logger.info("[REF-%d] Setup AVRCP.", i)
      avrcp_ext.setup_server(
          ref.device,
          avrcp_controller_handle=_AVRCP_CONTROLLER_RECORD_HANDLE,
          avrcp_target_handle=_AVRCP_TARGET_RECORD_HANDLE,
      )

  async def test_point_to_point_ag_and_a2dp(self) -> None:
    """Tests AG and A2DP connection to the same REF device.

    Test steps:
      1. Setup HF and A2DP on REF.
      2. Create bond from DUT.
      3. Wait for HFP and A2DP connected on DUT.
    """
    self.ref = self.refs[0]
    with (
        self.dut.bl4a.register_callback(_Module.A2DP) as dut_cb_a2dp,
        self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb_hfp,
    ):
      self._setup_headset_device(
          hf_configuration=hfp_ext.make_hf_configuration(),
          a2dp_codecs=[a2dp_ext.A2dpCodec.SBC],
      )

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

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

      self.logger.info("[DUT] Wait for HFP connected.")
      await dut_cb_hfp.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(self.ref.address)
      )

  @navi_test_base.named_parameterized(
      cvsd=dict(
          supported_audio_codecs=[_AudioCodec.CVSD],
      ),
      msbc=dict(
          supported_audio_codecs=[_AudioCodec.CVSD, _AudioCodec.MSBC],
      ),
      lc3_swb=dict(
          supported_audio_codecs=[
              _AudioCodec.LC3_SWB,
              _AudioCodec.CVSD,
              _AudioCodec.MSBC,
          ],
      ),
      handle_audio_focus=dict(
          supported_audio_codecs=[
              _AudioCodec.LC3_SWB,
              _AudioCodec.CVSD,
              _AudioCodec.MSBC,
          ],
          handle_audio_focus=True,
      ),
  )
  async def test_point_to_point_ag_call_during_a2dp(
      self,
      supported_audio_codecs: collections.abc.Sequence[hfp.AudioCodec],
      handle_audio_focus: bool = False,
  ) -> None:
    """Tests making an outgoing phone call while A2DP is playing.

    Test steps:
      1. Setup HFP and A2DP connection.
      2. Play sine and check A2DP is playing.
      3. Place an outgoing call.
      4. Check A2DP is stopped.
      5. Verify SCO connected.
      6. Terminate the call.
      7. Verify SCO disconnected.
      8. Verify A2DP resumed.

    Args:
      supported_audio_codecs: Audio codecs supported by REF device.
      handle_audio_focus: Whether to enable audio focus handling.
    """
    self.ref = self.refs[0]

    self.logger.info("[DUT] Set audio focus to %s.", handle_audio_focus)
    self.dut.bt.setAudioAttributes(None, handle_audio_focus)

    self._setup_headset_device(
        hf_configuration=hfp_ext.make_hf_configuration(
            supported_hf_features=[hfp.HfFeature.CODEC_NEGOTIATION],
            supported_audio_codecs=supported_audio_codecs,
        ),
        a2dp_codecs=[a2dp_ext.A2dpCodec.SBC],
    )

    dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
    dut_a2dp_cb = self.dut.bl4a.register_callback(_Module.A2DP)
    dut_player_cb = self.dut.bl4a.register_callback(_Module.PLAYER)
    self.test_case_context.push(dut_hfp_cb)
    self.test_case_context.push(dut_a2dp_cb)
    self.test_case_context.push(dut_player_cb)

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

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

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

    self.logger.info("[DUT] Set repeat mode to all.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)

    self.logger.info("[DUT] Start stream.")
    self.dut.bt.audioPlaySine()

    self.logger.info("[DUT] Check A2DP is playing.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.ref.address, state=android_constants.A2dpState.PLAYING
        ),
    )

    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.")
    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME,
        _CALLER_NUMBER,
        constants.Direction.OUTGOING,
    )

    with call:
      self.logger.info("[DUT] Check A2DP is not playing.")
      await dut_a2dp_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              address=self.ref.address,
              state=android_constants.A2dpState.NOT_PLAYING,
          ),
      )

      self.logger.info("[DUT] Wait for SCO connected.")
      await dut_hfp_cb.wait_for_event(
          _HfpAgAudioStateChange(
              address=self.ref.address, state=_ScoState.CONNECTED
          ),
      )

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

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

      sco_disconnected = asyncio.Event()
      sco_link.once(
          sco_link.EVENT_DISCONNECTION, lambda *_: sco_disconnected.set()
      )

      self.logger.info("[DUT] Terminate call.")
      call.close()

    self.logger.info("[DUT] Wait for SCO disconnected.")
    await dut_hfp_cb.wait_for_event(
        _HfpAgAudioStateChange(
            address=self.ref.address, state=_ScoState.DISCONNECTED
        ),
    )

    self.logger.info("[REF] Wait for SCO disconnected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await sco_disconnected.wait()

    self.logger.info("[DUT] Wait for A2DP resume.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.ref.address, state=android_constants.A2dpState.PLAYING
        ),
    )

    if handle_audio_focus:
      self.logger.info("[DUT] Wait for player resumed.")
      await dut_player_cb.wait_for_event(
          bl4a_api.PlayerIsPlayingChanged(is_playing=True),
      )

  @navi_test_base.parameterized(
      hfp.AudioCodec.CVSD,
      hfp.AudioCodec.MSBC,
      hfp.AudioCodec.LC3_SWB,
  )
  async def test_multipoint_hf_call_during_a2dp(
      self,
      codec: hfp.AudioCodec,
  ) -> None:
    """Tests an incoming phone call from phone while A2DP is playing on buds.

    Test steps:
      1. Setup a2dp connection on REF-0 and hfp connection on REF-1.
      2. Play sine and check A2DP is playing on REF-0.
      3. Place an incoming call to REF-1.
      4. Check A2DP is stopped.
      5. Verify SCO connected.
      6. Terminate the call.
      7. Verify SCO disconnected.
      8. Verify A2DP resumed.

    Args:
      codec: Audio codec to be negotiated.
    """
    if self.dut.getprop(android_constants.Property.HFP_HF_ENABLED) != "true":
      self.skipTest("DUT does not have HFP HF enabled.")

    self.logger.info("[DUT] Enable audio focus handling.")
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.MEDIA),
        handle_audio_focus=True,
    )

    self.logger.info("[REF-0] Setup A2DP sink.")
    a2dp_ext.setup_sink_server(
        self.refs[0].device,
        [a2dp_ext.A2dpCodec.SBC.get_default_capabilities()],
        _A2DP_SERVICE_RECORD_HANDLE,
    )
    avrcp_ext.setup_server(
        self.refs[0].device,
        avrcp_controller_handle=_AVRCP_CONTROLLER_RECORD_HANDLE,
        avrcp_target_handle=_AVRCP_TARGET_RECORD_HANDLE,
    )

    self.logger.info("[REF-1] Setup HFP AG.")
    ag_configuration = hfp_ext.make_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,
        ],
        supported_ag_indicators=[
            hfp.AgIndicatorState.call(),
            hfp.AgIndicatorState.callsetup(),
        ],
    )

    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:
        esco_parameters = hfp_ext.ESCO_PARAMETERS_LC3_T2
        if self.dut.getprop(_PROPERTY_SWB_SUPPORTED) != "true":
          self.skipTest("SWB is not supported on the device.")

    ref_hfp_protocols = asyncio.Queue[hfp.AgProtocol]()

    def on_dlc(dlc: rfcomm.DLC):
      ref_hfp_protocols.put_nowait(hfp.AgProtocol(dlc, ag_configuration))

    self.refs[1].device.sdp_service_records = {
        _HFP_AG_SDP_HANDLE: hfp_ext.AudioGatewaySdpRecord(
            service_record_handle=_HFP_AG_SDP_HANDLE,
            rfcomm_channel=rfcomm.Server(self.refs[1].device).listen(on_dlc),
            version=hfp.ProfileVersion.V1_8,
            supported_features=hfp_ext.make_ag_sdp_features(ag_configuration),
        )
    }

    dut_hf_cb = self.dut.bl4a.register_callback(_Module.HFP_HF)
    dut_a2dp_cb = self.dut.bl4a.register_callback(_Module.A2DP)
    dut_player_cb = self.dut.bl4a.register_callback(_Module.PLAYER)
    dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
    self.test_case_context.push(dut_hf_cb)
    self.test_case_context.push(dut_a2dp_cb)
    self.test_case_context.push(dut_player_cb)
    self.test_case_context.push(dut_telecom_cb)

    self.logger.info("[DUT] Connect and pair REF-0.")
    await self.classic_connect_and_pair(self.refs[0], connect_profiles=True)

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

    self.logger.info("[DUT] Connect and pair REF-1.")
    await self.classic_connect_and_pair(
        self.refs[1],
        connect_profiles=True,
    )

    self.logger.info("[DUT] Wait for HFP connected to REF-1.")
    await dut_hf_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.refs[1].address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF-1] Wait for HFP AG protocol connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hfp_protocol = await ref_hfp_protocols.get()

    self.logger.info("[DUT] Set repeat mode to ALL.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)

    self.logger.info("[DUT] Start stream.")
    self.dut.bt.audioPlaySine()

    self.logger.info("[DUT] Check A2DP is playing.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.refs[0].address,
            state=android_constants.A2dpState.PLAYING,
        ),
    )

    self.logger.info("[REF-1] 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 dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=_CallState.RINGING,
        )
    )

    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()

    self.logger.info("[REF] Accept call.")
    call_info.status = hfp.CallInfoStatus.ACTIVE
    ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 1)

    self.logger.info("[DUT] Wait for call state changed.")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=_CallState.ACTIVE,
        )
    )

    # Wait for A2DP to stop before setting up SCO, or some controllers may not
    # be able to accept the SCO connection when A2DP offloading is active.
    self.logger.info("[DUT] Check A2DP is not playing.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.refs[0].address,
            state=android_constants.A2dpState.NOT_PLAYING,
        ),
    )

    self.logger.info("[REF] Negotiate codec.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref_hfp_protocol.negotiate_codec(codec)

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

    self.logger.info("[REF] Create SCO.")
    connection = ref_hfp_protocol.dlc.multiplexer.l2cap_channel.connection
    await self.refs[1].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_hf_cb.wait_for_event(
        bl4a_api.HfpHfAudioStateChanged(
            address=self.refs[1].address, state=_HfpState.CONNECTED
        ),
    )

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

    self.logger.info("[REF] End call.")
    ref_hfp_protocol.calls.clear()
    ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 0)

    self.logger.info("[DUT] Wait for call disconnected.")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=_CallState.DISCONNECTED,
        )
    )

    # DUT may disconnect SCO before REF.
    if sco_link in sco_link.device.sco_links.values():
      self.logger.info("[REF] Disconnect SCO.")
      with contextlib.suppress(hci.HCI_StatusError):
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await sco_link.disconnect()

    self.logger.info("[DUT] Wait for SCO disconnected.")
    await dut_hf_cb.wait_for_event(
        bl4a_api.HfpHfAudioStateChanged(
            address=self.refs[1].address, state=_HfpState.DISCONNECTED
        ),
    )

    self.logger.info("[DUT] Wait for A2DP resume.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.refs[0].address,
            state=android_constants.A2dpState.PLAYING,
        ),
    )

  async def test_multidevice_hf_switch(self) -> None:
    """Tests DUT switch active hfp devices.

    Test steps:
      1. Setup two HFP HF devices.
      2. DUT pair with REF0.
      3. DUT pair with REF1.
      4. DUT make outgoing call.
      5. DUT answer the call.
      6. DUT switch active device to REF0.
      7. DUT switch active device to REF1.
    """
    if self.dut.bt.maxConnectedAudioDevices() < 2:
      self.skipTest("[DUT] Multi-device HF is not supported.")

    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
      for i, ref in enumerate(self.refs):
        self.logger.info("[REF-%d] Setup HFP HF", i)
        hfp_ext.HfProtocol.setup_server(
            ref.device,
            sdp_handle=_HFP_HF_SDP_HANDLE,
            configuration=hfp_ext.make_hf_configuration(),
        )

        await self.classic_connect_and_pair(ref, connect_profiles=True)

        self.logger.info("[DUT] Wait for HFP connected to REF-%d", i)
        await dut_hfp_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(address=ref.address),
        )

    with (
        self.dut.bl4a.register_callback(
            bl4a_api.Module.TELECOM
        ) as dut_telecom_cb,
        self.dut.bl4a.make_phone_call(
            _CALLER_NAME,
            _CALLER_NUMBER,
            constants.Direction.OUTGOING,
        ) as call,
    ):
      self.logger.info("[DUT] Wait for call dialing.")
      await dut_telecom_cb.wait_for_event(
          bl4a_api.CallStateChanged(
              handle=mock.ANY,
              name=mock.ANY,
              state=android_constants.CallState.DIALING,
          ),
      )

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

      self.logger.info("[DUT] Wait for call active.")
      await dut_telecom_cb.wait_for_event(
          bl4a_api.CallStateChanged(
              handle=mock.ANY,
              name=mock.ANY,
              state=android_constants.CallState.ACTIVE,
          ),
      )

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

      # The default route should be REF1.
      for i, ref in enumerate(self.refs):
        with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
          self.assertNotEqual(
              self.dut.bt.hfpAgGetAudioState(ref.address),
              _ScoState.CONNECTED,
              f"SCO is already connected to REF{i}.",
          )

          self.logger.info("[DUT] Switch to REF-%d", i)
          self.dut.bt.setActiveDevice(
              ref.address,
              android_constants.ActiveDeviceUse.PHONE_CALL,
          )

          self.logger.info("[DUT] Wait for HFP connected to REF-%d", i)
          await dut_hfp_cb.wait_for_event(
              bl4a_api.ProfileActiveDeviceChanged(ref.address)
          )

          self.logger.info("[DUT] Wait for SCO connected to REF-%d", i)
          await dut_hfp_cb.wait_for_event(
              event=_HfpAgAudioStateChange(
                  address=ref.address, state=_ScoState.CONNECTED
              ),
          )

      self.logger.info("[DUT] Terminate call.")
      call.close()

  async def test_multidevice_a2dp_switch(self) -> None:
    """Tests DUT switch active a2dp devices.

    Test steps:
      1. Setup two A2DP devices.
      2. DUT pair with REF0.
      3. DUT pair with REF1.
      4. DUT switch active device to REF0.
      5. DUT switch active device to REF1.
    """
    if self.dut.bt.maxConnectedAudioDevices() < 2:
      self.skipTest("[DUT] Multi-device A2DP is not supported.")

    with self.dut.bl4a.register_callback(_Module.A2DP) as dut_a2dp_cb:
      for i, ref in enumerate(self.refs):
        self.logger.info("[REF-%d] Setup A2DP", i)
        a2dp_ext.setup_sink_server(
            ref.device,
            [a2dp_ext.A2dpCodec.SBC.get_default_capabilities()],
            _A2DP_SERVICE_RECORD_HANDLE,
        )

        await self.classic_connect_and_pair(ref, connect_profiles=True)

        self.logger.info("[DUT] Wait for A2DP connected to REF-%d", i)
        await dut_a2dp_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(address=ref.address),
        )

    with self.dut.bl4a.register_callback(_Module.A2DP) as dut_a2dp_cb:
      self.logger.info("[DUT] Start playing music.")
      self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
      await asyncio.to_thread(self.dut.bt.audioPlaySine)

      if not self.dut.bt.isA2dpPlaying(self.refs[1].address):
        self.logger.info("[DUT] Wait for A2DP playing on REF-%1.")
        await dut_a2dp_cb.wait_for_event(
            bl4a_api.A2dpPlayingStateChanged(
                self.refs[1].address, android_constants.A2dpState.PLAYING
            ),
        )

      # The default route should be REF-1.
      for i, ref in enumerate(self.refs):
        self.assertFalse(
            self.dut.bt.isA2dpPlaying(ref.address),
            f"A2DP is already playing on REF{i}.",
        )

        self.logger.info("[DUT] Switch to REF-%d", i)
        self.dut.bt.setActiveDevice(
            ref.address,
            android_constants.ActiveDeviceUse.AUDIO,
        )

        self.logger.info("[DUT] Wait for A2DP connected to REF-%d", i)
        await dut_a2dp_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(ref.address)
        )

        if not self.dut.bt.isA2dpPlaying(ref.address):
          self.logger.info("[DUT] Wait for A2DP playing on REF-%d.", i)
          await dut_a2dp_cb.wait_for_event(
              bl4a_api.A2dpPlayingStateChanged(
                  ref.address, android_constants.A2dpState.PLAYING
              ),
          )

  async def test_multidevice_lea_switch(self) -> None:
    """Tests DUT switch active LEA devices.

    Test steps:
      1. Setup two LEA devices.
      2. DUT pair with REF0.
      3. DUT pair with REF1.
      4. Play music on DUT.
      5. Wait for music to start on REF1.
      6. DUT switch active device to REF0.
      7. DUT switch active device to REF1.
    """
    if self.dut.bt.maxConnectedAudioDevices() < 2:
      self.skipTest("[DUT] Multi-device LEA is not supported.")
    if not self.dut.is_le_audio_supported:
      self.skipTest("[DUT] Unicast client is not enabled")

    for ref in self.refs:
      ref.config.cis_enabled = True
      ref.device.cis_enabled = True

    async with self.assert_not_timeout(_SETUP_TIMEOUT_SECONDS):
      await asyncio.gather(
          *[ref.reset() for ref in self.refs],
      )

    self.logger.info("[DUT] Set audio attributes to media.")
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.MEDIA),
        handle_audio_focus=False,
    )

    sink_ase = list[ascs.AudioStreamEndpointCharacteristic]()

    with self.dut.bl4a.register_callback(_Module.LE_AUDIO) as dut_lea_cb:
      for i, ref in enumerate(self.refs):
        self.logger.info("[REF-%d] Setup LEA", i)
        ref.device.add_service(pacs.make_pacs())
        ref_ascs = ascs.AudioStreamControlService(
            ref.device,
            sink_ase_id=[_SINK_ASE_ID],
            source_ase_id=[_SOURCE_ASE_ID],
        )
        ref_vcs = vcs.VolumeControlService(volume_setting=vcs.MAX_VOLUME // 2)
        ref.device.add_service(ref_ascs)
        ref.device.add_service(ref_vcs)
        ref.device.add_service(
            gmap.GamingAudioService(
                gmap_role=gmap.GmapRole.UNICAST_GAME_TERMINAL,
                ugt_features=(
                    gmap.UgtFeatures.UGT_SOURCE | gmap.UgtFeatures.UGT_SINK
                ),
            )
        )

        sink_ase.append(ref_ascs.ase_state_machines[_SINK_ASE_ID])

        self.logger.info("[DUT] Connect and pair REF-%d.", i)
        await self.le_connect_and_pair(
            ref_address_type=hci.OwnAddressType.RANDOM,
            ref=ref,
            connect_profiles=True,
        )

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

        self.logger.info("[DUT] Wait for audio route ready")
        await dut_lea_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(ref.random_address)
        )

    self.logger.info("[DUT] Set repeat mode to one.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)

    self.logger.info("[DUT] Start playing music.")
    self.dut.bt.audioPlaySine()

    # The default route should be REF-1.
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF-1] Wait for audio to start",
    ):
      await _wait_for_ase_state(
          sink_ase[1], ascs.AudioStreamEndpointCharacteristic.State.STREAMING
      )

    # Wait for the ase of dut to enter streaming.
    await asyncio.sleep(0.5)

    for i, ref in enumerate(self.refs):
      with self.dut.bl4a.register_callback(_Module.LE_AUDIO) as dut_lea_cb:
        self.logger.info("[DUT] Switch audio route to REF-%d", i)
        self.dut.bt.setActiveDevice(
            ref.random_address,
            android_constants.ActiveDeviceUse.ALL,
        )

        self.logger.info("[DUT] Wait for LEA set active on REF-%d", i)
        await dut_lea_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(ref.random_address)
        )

      self.logger.info("[DUT] Wait for LEA playing on REF-%d.", i)
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg=f"[REF-{i}] Wait for audio to start",
      ):
        await _wait_for_ase_state(
            sink_ase[i], ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )

  async def test_multipoint_ringtone(self) -> None:
    """Tests phone call, ringtone is played on both REF-HF and DUT.

    Test steps:
      1. Setup HFP HF on REF-HF.
      2. Setup HFP AG on REF-AG.
      3. Connect and pair DUT to REF-HF.
      4. Connect and pair DUT to REF-AG.
      5. Make a phone call from REF-AG.
    """
    if self.dut.getprop(android_constants.Property.HFP_HF_ENABLED) != "true":
      self.skipTest("DUT does not have HFP HF enabled.")

    if self.dut.getprop(android_constants.Property.HFP_AG_ENABLED) != "true":
      self.skipTest("DUT does not have HFP AG enabled.")

    ref_hf_protocol_queue = hfp_ext.HfProtocol.setup_server(
        self.refs[0].device,
        sdp_handle=_HFP_HF_SDP_HANDLE,
        configuration=hfp_ext.make_hf_configuration(),
    )

    ref_ag_protocols = asyncio.Queue[hfp.AgProtocol]()

    def on_dlc(dlc: rfcomm.DLC):
      ref_ag_protocols.put_nowait(
          hfp.AgProtocol(dlc, hfp_ext.make_ag_configuration())
      )

    self.refs[1].device.sdp_service_records = {
        _HFP_AG_SDP_HANDLE: (
            hfp_ext.AudioGatewaySdpRecord(
                service_record_handle=_HFP_AG_SDP_HANDLE,
                rfcomm_channel=rfcomm.Server(self.refs[1].device).listen(
                    on_dlc
                ),
                version=hfp.ProfileVersion.V1_8,
                supported_features=hfp_ext.make_ag_sdp_features(
                    hfp_ext.make_ag_configuration()
                ),
            ).to_service_attributes()
        )
    }

    dut_ag_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
    dut_hf_cb = self.dut.bl4a.register_callback(_Module.HFP_HF)
    dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
    self.test_case_context.push(dut_ag_cb)
    self.test_case_context.push(dut_hf_cb)
    self.test_case_context.push(dut_telecom_cb)

    await self.classic_connect_and_pair(self.refs[0], connect_profiles=True)

    self.logger.info("[DUT] Wait for HFP AG connected on REF-HF.")
    await dut_ag_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.refs[0].address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF-HF] Wait for HF protocol connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_hf_protocol = await ref_hf_protocol_queue.get()

    ref_hf_ring_event = asyncio.Event()
    ref_hf_protocol.on(hfp.HfProtocol.EVENT_RING, ref_hf_ring_event.set)

    await self.classic_connect_and_pair(self.refs[1], connect_profiles=True)

    self.logger.info("[DUT] Wait for HFP HF connected on REF-AG.")
    await dut_hf_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.refs[1].address,
            state=android_constants.ConnectionState.CONNECTED,
        ),
    )

    self.logger.info("[REF-AG] Wait for AG protocol connected.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      ref_ag_protocol = await ref_ag_protocols.get()

    self.logger.info("[REF-AG] 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_ag_protocol.calls.append(call_info)
    ref_ag_protocol.update_ag_indicator(
        hfp.AgIndicator.CALL_SETUP,
        hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
    )

    self.logger.info("[DUT] Wait for call ringing.")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=_CallState.RINGING,
        )
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF-HF] Wait for ringtone.",
    ):
      await ref_hf_ring_event.wait()

  async def test_multipoint_call(self) -> None:
    """Tests phone call, SCO connection is only connected to REF-AG.

    Test steps:
      1. Setup HFP HF on REF-HF.
      2. Setup HFP AG on REF-AG.
      3. Connect and pair DUT to REF-HF.
      4. Connect and pair DUT to REF-AG.
      5. Make a phone call from REF-AG.
      6. Answer the call on DUT.
      7. Wait for SCO connected only on REF-AG.
    """
    await self.test_multipoint_ringtone()

    sco_link_hf = asyncio.Queue[device.ScoLink]()
    self.refs[0].device.on(
        self.refs[0].device.EVENT_SCO_CONNECTION, sco_link_hf.put_nowait
    )

    self.logger.info("[DUT] Answer call.")
    self.dut.shell("input keyevent KEYCODE_CALL")

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF-HF] Wait for SCO connected.")
      await sco_link_hf.get()

    self.logger.info("[REF-AG] Check SCO is not connected.")
    self.assertEmpty(self.refs[1].device.sco_links)

Tests DUT switch active a2dp devices.

Test steps
  1. Setup two A2DP devices.
  2. DUT pair with REF0.
  3. DUT pair with REF1.
  4. DUT switch active device to REF0.
  5. DUT switch active device to REF1.
Source code in navi/tests/functionality/coex_test.py
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
async def test_multidevice_a2dp_switch(self) -> None:
  """Tests DUT switch active a2dp devices.

  Test steps:
    1. Setup two A2DP devices.
    2. DUT pair with REF0.
    3. DUT pair with REF1.
    4. DUT switch active device to REF0.
    5. DUT switch active device to REF1.
  """
  if self.dut.bt.maxConnectedAudioDevices() < 2:
    self.skipTest("[DUT] Multi-device A2DP is not supported.")

  with self.dut.bl4a.register_callback(_Module.A2DP) as dut_a2dp_cb:
    for i, ref in enumerate(self.refs):
      self.logger.info("[REF-%d] Setup A2DP", i)
      a2dp_ext.setup_sink_server(
          ref.device,
          [a2dp_ext.A2dpCodec.SBC.get_default_capabilities()],
          _A2DP_SERVICE_RECORD_HANDLE,
      )

      await self.classic_connect_and_pair(ref, connect_profiles=True)

      self.logger.info("[DUT] Wait for A2DP connected to REF-%d", i)
      await dut_a2dp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=ref.address),
      )

  with self.dut.bl4a.register_callback(_Module.A2DP) as dut_a2dp_cb:
    self.logger.info("[DUT] Start playing music.")
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)
    await asyncio.to_thread(self.dut.bt.audioPlaySine)

    if not self.dut.bt.isA2dpPlaying(self.refs[1].address):
      self.logger.info("[DUT] Wait for A2DP playing on REF-%1.")
      await dut_a2dp_cb.wait_for_event(
          bl4a_api.A2dpPlayingStateChanged(
              self.refs[1].address, android_constants.A2dpState.PLAYING
          ),
      )

    # The default route should be REF-1.
    for i, ref in enumerate(self.refs):
      self.assertFalse(
          self.dut.bt.isA2dpPlaying(ref.address),
          f"A2DP is already playing on REF{i}.",
      )

      self.logger.info("[DUT] Switch to REF-%d", i)
      self.dut.bt.setActiveDevice(
          ref.address,
          android_constants.ActiveDeviceUse.AUDIO,
      )

      self.logger.info("[DUT] Wait for A2DP connected to REF-%d", i)
      await dut_a2dp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(ref.address)
      )

      if not self.dut.bt.isA2dpPlaying(ref.address):
        self.logger.info("[DUT] Wait for A2DP playing on REF-%d.", i)
        await dut_a2dp_cb.wait_for_event(
            bl4a_api.A2dpPlayingStateChanged(
                ref.address, android_constants.A2dpState.PLAYING
            ),
        )

Tests DUT switch active hfp devices.

Test steps
  1. Setup two HFP HF devices.
  2. DUT pair with REF0.
  3. DUT pair with REF1.
  4. DUT make outgoing call.
  5. DUT answer the call.
  6. DUT switch active device to REF0.
  7. DUT switch active device to REF1.
Source code in navi/tests/functionality/coex_test.py
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
async def test_multidevice_hf_switch(self) -> None:
  """Tests DUT switch active hfp devices.

  Test steps:
    1. Setup two HFP HF devices.
    2. DUT pair with REF0.
    3. DUT pair with REF1.
    4. DUT make outgoing call.
    5. DUT answer the call.
    6. DUT switch active device to REF0.
    7. DUT switch active device to REF1.
  """
  if self.dut.bt.maxConnectedAudioDevices() < 2:
    self.skipTest("[DUT] Multi-device HF is not supported.")

  with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
    for i, ref in enumerate(self.refs):
      self.logger.info("[REF-%d] Setup HFP HF", i)
      hfp_ext.HfProtocol.setup_server(
          ref.device,
          sdp_handle=_HFP_HF_SDP_HANDLE,
          configuration=hfp_ext.make_hf_configuration(),
      )

      await self.classic_connect_and_pair(ref, connect_profiles=True)

      self.logger.info("[DUT] Wait for HFP connected to REF-%d", i)
      await dut_hfp_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(address=ref.address),
      )

  with (
      self.dut.bl4a.register_callback(
          bl4a_api.Module.TELECOM
      ) as dut_telecom_cb,
      self.dut.bl4a.make_phone_call(
          _CALLER_NAME,
          _CALLER_NUMBER,
          constants.Direction.OUTGOING,
      ) as call,
  ):
    self.logger.info("[DUT] Wait for call dialing.")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=android_constants.CallState.DIALING,
        ),
    )

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

    self.logger.info("[DUT] Wait for call active.")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            handle=mock.ANY,
            name=mock.ANY,
            state=android_constants.CallState.ACTIVE,
        ),
    )

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

    # The default route should be REF1.
    for i, ref in enumerate(self.refs):
      with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_hfp_cb:
        self.assertNotEqual(
            self.dut.bt.hfpAgGetAudioState(ref.address),
            _ScoState.CONNECTED,
            f"SCO is already connected to REF{i}.",
        )

        self.logger.info("[DUT] Switch to REF-%d", i)
        self.dut.bt.setActiveDevice(
            ref.address,
            android_constants.ActiveDeviceUse.PHONE_CALL,
        )

        self.logger.info("[DUT] Wait for HFP connected to REF-%d", i)
        await dut_hfp_cb.wait_for_event(
            bl4a_api.ProfileActiveDeviceChanged(ref.address)
        )

        self.logger.info("[DUT] Wait for SCO connected to REF-%d", i)
        await dut_hfp_cb.wait_for_event(
            event=_HfpAgAudioStateChange(
                address=ref.address, state=_ScoState.CONNECTED
            ),
        )

    self.logger.info("[DUT] Terminate call.")
    call.close()

Tests DUT switch active LEA devices.

Test steps
  1. Setup two LEA devices.
  2. DUT pair with REF0.
  3. DUT pair with REF1.
  4. Play music on DUT.
  5. Wait for music to start on REF1.
  6. DUT switch active device to REF0.
  7. DUT switch active device to REF1.
Source code in navi/tests/functionality/coex_test.py
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
async def test_multidevice_lea_switch(self) -> None:
  """Tests DUT switch active LEA devices.

  Test steps:
    1. Setup two LEA devices.
    2. DUT pair with REF0.
    3. DUT pair with REF1.
    4. Play music on DUT.
    5. Wait for music to start on REF1.
    6. DUT switch active device to REF0.
    7. DUT switch active device to REF1.
  """
  if self.dut.bt.maxConnectedAudioDevices() < 2:
    self.skipTest("[DUT] Multi-device LEA is not supported.")
  if not self.dut.is_le_audio_supported:
    self.skipTest("[DUT] Unicast client is not enabled")

  for ref in self.refs:
    ref.config.cis_enabled = True
    ref.device.cis_enabled = True

  async with self.assert_not_timeout(_SETUP_TIMEOUT_SECONDS):
    await asyncio.gather(
        *[ref.reset() for ref in self.refs],
    )

  self.logger.info("[DUT] Set audio attributes to media.")
  self.dut.bl4a.set_audio_attributes(
      bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.MEDIA),
      handle_audio_focus=False,
  )

  sink_ase = list[ascs.AudioStreamEndpointCharacteristic]()

  with self.dut.bl4a.register_callback(_Module.LE_AUDIO) as dut_lea_cb:
    for i, ref in enumerate(self.refs):
      self.logger.info("[REF-%d] Setup LEA", i)
      ref.device.add_service(pacs.make_pacs())
      ref_ascs = ascs.AudioStreamControlService(
          ref.device,
          sink_ase_id=[_SINK_ASE_ID],
          source_ase_id=[_SOURCE_ASE_ID],
      )
      ref_vcs = vcs.VolumeControlService(volume_setting=vcs.MAX_VOLUME // 2)
      ref.device.add_service(ref_ascs)
      ref.device.add_service(ref_vcs)
      ref.device.add_service(
          gmap.GamingAudioService(
              gmap_role=gmap.GmapRole.UNICAST_GAME_TERMINAL,
              ugt_features=(
                  gmap.UgtFeatures.UGT_SOURCE | gmap.UgtFeatures.UGT_SINK
              ),
          )
      )

      sink_ase.append(ref_ascs.ase_state_machines[_SINK_ASE_ID])

      self.logger.info("[DUT] Connect and pair REF-%d.", i)
      await self.le_connect_and_pair(
          ref_address_type=hci.OwnAddressType.RANDOM,
          ref=ref,
          connect_profiles=True,
      )

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

      self.logger.info("[DUT] Wait for audio route ready")
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(ref.random_address)
      )

  self.logger.info("[DUT] Set repeat mode to one.")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)

  self.logger.info("[DUT] Start playing music.")
  self.dut.bt.audioPlaySine()

  # The default route should be REF-1.
  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF-1] Wait for audio to start",
  ):
    await _wait_for_ase_state(
        sink_ase[1], ascs.AudioStreamEndpointCharacteristic.State.STREAMING
    )

  # Wait for the ase of dut to enter streaming.
  await asyncio.sleep(0.5)

  for i, ref in enumerate(self.refs):
    with self.dut.bl4a.register_callback(_Module.LE_AUDIO) as dut_lea_cb:
      self.logger.info("[DUT] Switch audio route to REF-%d", i)
      self.dut.bt.setActiveDevice(
          ref.random_address,
          android_constants.ActiveDeviceUse.ALL,
      )

      self.logger.info("[DUT] Wait for LEA set active on REF-%d", i)
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(ref.random_address)
      )

    self.logger.info("[DUT] Wait for LEA playing on REF-%d.", i)
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg=f"[REF-{i}] Wait for audio to start",
    ):
      await _wait_for_ase_state(
          sink_ase[i], ascs.AudioStreamEndpointCharacteristic.State.STREAMING
      )

Tests phone call, SCO connection is only connected to REF-AG.

Test steps
  1. Setup HFP HF on REF-HF.
  2. Setup HFP AG on REF-AG.
  3. Connect and pair DUT to REF-HF.
  4. Connect and pair DUT to REF-AG.
  5. Make a phone call from REF-AG.
  6. Answer the call on DUT.
  7. Wait for SCO connected only on REF-AG.
Source code in navi/tests/functionality/coex_test.py
 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
async def test_multipoint_call(self) -> None:
  """Tests phone call, SCO connection is only connected to REF-AG.

  Test steps:
    1. Setup HFP HF on REF-HF.
    2. Setup HFP AG on REF-AG.
    3. Connect and pair DUT to REF-HF.
    4. Connect and pair DUT to REF-AG.
    5. Make a phone call from REF-AG.
    6. Answer the call on DUT.
    7. Wait for SCO connected only on REF-AG.
  """
  await self.test_multipoint_ringtone()

  sco_link_hf = asyncio.Queue[device.ScoLink]()
  self.refs[0].device.on(
      self.refs[0].device.EVENT_SCO_CONNECTION, sco_link_hf.put_nowait
  )

  self.logger.info("[DUT] Answer call.")
  self.dut.shell("input keyevent KEYCODE_CALL")

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF-HF] Wait for SCO connected.")
    await sco_link_hf.get()

  self.logger.info("[REF-AG] Check SCO is not connected.")
  self.assertEmpty(self.refs[1].device.sco_links)

Tests an incoming phone call from phone while A2DP is playing on buds.

Test steps
  1. Setup a2dp connection on REF-0 and hfp connection on REF-1.
  2. Play sine and check A2DP is playing on REF-0.
  3. Place an incoming call to REF-1.
  4. Check A2DP is stopped.
  5. Verify SCO connected.
  6. Terminate the call.
  7. Verify SCO disconnected.
  8. Verify A2DP resumed.

Parameters:

Name Type Description Default
codec AudioCodec

Audio codec to be negotiated.

required
Source code in navi/tests/functionality/coex_test.py
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
@navi_test_base.parameterized(
    hfp.AudioCodec.CVSD,
    hfp.AudioCodec.MSBC,
    hfp.AudioCodec.LC3_SWB,
)
async def test_multipoint_hf_call_during_a2dp(
    self,
    codec: hfp.AudioCodec,
) -> None:
  """Tests an incoming phone call from phone while A2DP is playing on buds.

  Test steps:
    1. Setup a2dp connection on REF-0 and hfp connection on REF-1.
    2. Play sine and check A2DP is playing on REF-0.
    3. Place an incoming call to REF-1.
    4. Check A2DP is stopped.
    5. Verify SCO connected.
    6. Terminate the call.
    7. Verify SCO disconnected.
    8. Verify A2DP resumed.

  Args:
    codec: Audio codec to be negotiated.
  """
  if self.dut.getprop(android_constants.Property.HFP_HF_ENABLED) != "true":
    self.skipTest("DUT does not have HFP HF enabled.")

  self.logger.info("[DUT] Enable audio focus handling.")
  self.dut.bl4a.set_audio_attributes(
      bl4a_api.AudioAttributes(usage=bl4a_api.AudioAttributes.Usage.MEDIA),
      handle_audio_focus=True,
  )

  self.logger.info("[REF-0] Setup A2DP sink.")
  a2dp_ext.setup_sink_server(
      self.refs[0].device,
      [a2dp_ext.A2dpCodec.SBC.get_default_capabilities()],
      _A2DP_SERVICE_RECORD_HANDLE,
  )
  avrcp_ext.setup_server(
      self.refs[0].device,
      avrcp_controller_handle=_AVRCP_CONTROLLER_RECORD_HANDLE,
      avrcp_target_handle=_AVRCP_TARGET_RECORD_HANDLE,
  )

  self.logger.info("[REF-1] Setup HFP AG.")
  ag_configuration = hfp_ext.make_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,
      ],
      supported_ag_indicators=[
          hfp.AgIndicatorState.call(),
          hfp.AgIndicatorState.callsetup(),
      ],
  )

  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:
      esco_parameters = hfp_ext.ESCO_PARAMETERS_LC3_T2
      if self.dut.getprop(_PROPERTY_SWB_SUPPORTED) != "true":
        self.skipTest("SWB is not supported on the device.")

  ref_hfp_protocols = asyncio.Queue[hfp.AgProtocol]()

  def on_dlc(dlc: rfcomm.DLC):
    ref_hfp_protocols.put_nowait(hfp.AgProtocol(dlc, ag_configuration))

  self.refs[1].device.sdp_service_records = {
      _HFP_AG_SDP_HANDLE: hfp_ext.AudioGatewaySdpRecord(
          service_record_handle=_HFP_AG_SDP_HANDLE,
          rfcomm_channel=rfcomm.Server(self.refs[1].device).listen(on_dlc),
          version=hfp.ProfileVersion.V1_8,
          supported_features=hfp_ext.make_ag_sdp_features(ag_configuration),
      )
  }

  dut_hf_cb = self.dut.bl4a.register_callback(_Module.HFP_HF)
  dut_a2dp_cb = self.dut.bl4a.register_callback(_Module.A2DP)
  dut_player_cb = self.dut.bl4a.register_callback(_Module.PLAYER)
  dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
  self.test_case_context.push(dut_hf_cb)
  self.test_case_context.push(dut_a2dp_cb)
  self.test_case_context.push(dut_player_cb)
  self.test_case_context.push(dut_telecom_cb)

  self.logger.info("[DUT] Connect and pair REF-0.")
  await self.classic_connect_and_pair(self.refs[0], connect_profiles=True)

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

  self.logger.info("[DUT] Connect and pair REF-1.")
  await self.classic_connect_and_pair(
      self.refs[1],
      connect_profiles=True,
  )

  self.logger.info("[DUT] Wait for HFP connected to REF-1.")
  await dut_hf_cb.wait_for_event(
      bl4a_api.ProfileConnectionStateChanged(
          address=self.refs[1].address,
          state=android_constants.ConnectionState.CONNECTED,
      ),
  )

  self.logger.info("[REF-1] Wait for HFP AG protocol connected.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_hfp_protocol = await ref_hfp_protocols.get()

  self.logger.info("[DUT] Set repeat mode to ALL.")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)

  self.logger.info("[DUT] Start stream.")
  self.dut.bt.audioPlaySine()

  self.logger.info("[DUT] Check A2DP is playing.")
  await dut_a2dp_cb.wait_for_event(
      bl4a_api.A2dpPlayingStateChanged(
          address=self.refs[0].address,
          state=android_constants.A2dpState.PLAYING,
      ),
  )

  self.logger.info("[REF-1] 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 dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=_CallState.RINGING,
      )
  )

  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()

  self.logger.info("[REF] Accept call.")
  call_info.status = hfp.CallInfoStatus.ACTIVE
  ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 1)

  self.logger.info("[DUT] Wait for call state changed.")
  await dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=_CallState.ACTIVE,
      )
  )

  # Wait for A2DP to stop before setting up SCO, or some controllers may not
  # be able to accept the SCO connection when A2DP offloading is active.
  self.logger.info("[DUT] Check A2DP is not playing.")
  await dut_a2dp_cb.wait_for_event(
      bl4a_api.A2dpPlayingStateChanged(
          address=self.refs[0].address,
          state=android_constants.A2dpState.NOT_PLAYING,
      ),
  )

  self.logger.info("[REF] Negotiate codec.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await ref_hfp_protocol.negotiate_codec(codec)

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

  self.logger.info("[REF] Create SCO.")
  connection = ref_hfp_protocol.dlc.multiplexer.l2cap_channel.connection
  await self.refs[1].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_hf_cb.wait_for_event(
      bl4a_api.HfpHfAudioStateChanged(
          address=self.refs[1].address, state=_HfpState.CONNECTED
      ),
  )

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

  self.logger.info("[REF] End call.")
  ref_hfp_protocol.calls.clear()
  ref_hfp_protocol.update_ag_indicator(hfp.AgIndicator.CALL, 0)

  self.logger.info("[DUT] Wait for call disconnected.")
  await dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=_CallState.DISCONNECTED,
      )
  )

  # DUT may disconnect SCO before REF.
  if sco_link in sco_link.device.sco_links.values():
    self.logger.info("[REF] Disconnect SCO.")
    with contextlib.suppress(hci.HCI_StatusError):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await sco_link.disconnect()

  self.logger.info("[DUT] Wait for SCO disconnected.")
  await dut_hf_cb.wait_for_event(
      bl4a_api.HfpHfAudioStateChanged(
          address=self.refs[1].address, state=_HfpState.DISCONNECTED
      ),
  )

  self.logger.info("[DUT] Wait for A2DP resume.")
  await dut_a2dp_cb.wait_for_event(
      bl4a_api.A2dpPlayingStateChanged(
          address=self.refs[0].address,
          state=android_constants.A2dpState.PLAYING,
      ),
  )

Tests phone call, ringtone is played on both REF-HF and DUT.

Test steps
  1. Setup HFP HF on REF-HF.
  2. Setup HFP AG on REF-AG.
  3. Connect and pair DUT to REF-HF.
  4. Connect and pair DUT to REF-AG.
  5. Make a phone call from REF-AG.
Source code in navi/tests/functionality/coex_test.py
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
async def test_multipoint_ringtone(self) -> None:
  """Tests phone call, ringtone is played on both REF-HF and DUT.

  Test steps:
    1. Setup HFP HF on REF-HF.
    2. Setup HFP AG on REF-AG.
    3. Connect and pair DUT to REF-HF.
    4. Connect and pair DUT to REF-AG.
    5. Make a phone call from REF-AG.
  """
  if self.dut.getprop(android_constants.Property.HFP_HF_ENABLED) != "true":
    self.skipTest("DUT does not have HFP HF enabled.")

  if self.dut.getprop(android_constants.Property.HFP_AG_ENABLED) != "true":
    self.skipTest("DUT does not have HFP AG enabled.")

  ref_hf_protocol_queue = hfp_ext.HfProtocol.setup_server(
      self.refs[0].device,
      sdp_handle=_HFP_HF_SDP_HANDLE,
      configuration=hfp_ext.make_hf_configuration(),
  )

  ref_ag_protocols = asyncio.Queue[hfp.AgProtocol]()

  def on_dlc(dlc: rfcomm.DLC):
    ref_ag_protocols.put_nowait(
        hfp.AgProtocol(dlc, hfp_ext.make_ag_configuration())
    )

  self.refs[1].device.sdp_service_records = {
      _HFP_AG_SDP_HANDLE: (
          hfp_ext.AudioGatewaySdpRecord(
              service_record_handle=_HFP_AG_SDP_HANDLE,
              rfcomm_channel=rfcomm.Server(self.refs[1].device).listen(
                  on_dlc
              ),
              version=hfp.ProfileVersion.V1_8,
              supported_features=hfp_ext.make_ag_sdp_features(
                  hfp_ext.make_ag_configuration()
              ),
          ).to_service_attributes()
      )
  }

  dut_ag_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
  dut_hf_cb = self.dut.bl4a.register_callback(_Module.HFP_HF)
  dut_telecom_cb = self.dut.bl4a.register_callback(_Module.TELECOM)
  self.test_case_context.push(dut_ag_cb)
  self.test_case_context.push(dut_hf_cb)
  self.test_case_context.push(dut_telecom_cb)

  await self.classic_connect_and_pair(self.refs[0], connect_profiles=True)

  self.logger.info("[DUT] Wait for HFP AG connected on REF-HF.")
  await dut_ag_cb.wait_for_event(
      bl4a_api.ProfileConnectionStateChanged(
          address=self.refs[0].address,
          state=android_constants.ConnectionState.CONNECTED,
      ),
  )

  self.logger.info("[REF-HF] Wait for HF protocol connected.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_hf_protocol = await ref_hf_protocol_queue.get()

  ref_hf_ring_event = asyncio.Event()
  ref_hf_protocol.on(hfp.HfProtocol.EVENT_RING, ref_hf_ring_event.set)

  await self.classic_connect_and_pair(self.refs[1], connect_profiles=True)

  self.logger.info("[DUT] Wait for HFP HF connected on REF-AG.")
  await dut_hf_cb.wait_for_event(
      bl4a_api.ProfileConnectionStateChanged(
          address=self.refs[1].address,
          state=android_constants.ConnectionState.CONNECTED,
      ),
  )

  self.logger.info("[REF-AG] Wait for AG protocol connected.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    ref_ag_protocol = await ref_ag_protocols.get()

  self.logger.info("[REF-AG] 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_ag_protocol.calls.append(call_info)
  ref_ag_protocol.update_ag_indicator(
      hfp.AgIndicator.CALL_SETUP,
      hfp.CallSetupAgIndicator.INCOMING_CALL_PROCESS,
  )

  self.logger.info("[DUT] Wait for call ringing.")
  await dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          handle=mock.ANY,
          name=mock.ANY,
          state=_CallState.RINGING,
      )
  )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS,
      msg="[REF-HF] Wait for ringtone.",
  ):
    await ref_hf_ring_event.wait()

Tests AG and A2DP connection to the same REF device.

Test steps
  1. Setup HF and A2DP on REF.
  2. Create bond from DUT.
  3. Wait for HFP and A2DP connected on DUT.
Source code in navi/tests/functionality/coex_test.py
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
async def test_point_to_point_ag_and_a2dp(self) -> None:
  """Tests AG and A2DP connection to the same REF device.

  Test steps:
    1. Setup HF and A2DP on REF.
    2. Create bond from DUT.
    3. Wait for HFP and A2DP connected on DUT.
  """
  self.ref = self.refs[0]
  with (
      self.dut.bl4a.register_callback(_Module.A2DP) as dut_cb_a2dp,
      self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb_hfp,
  ):
    self._setup_headset_device(
        hf_configuration=hfp_ext.make_hf_configuration(),
        a2dp_codecs=[a2dp_ext.A2dpCodec.SBC],
    )

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

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

    self.logger.info("[DUT] Wait for HFP connected.")
    await dut_cb_hfp.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(self.ref.address)
    )

Tests making an outgoing phone call while A2DP is playing.

Test steps
  1. Setup HFP and A2DP connection.
  2. Play sine and check A2DP is playing.
  3. Place an outgoing call.
  4. Check A2DP is stopped.
  5. Verify SCO connected.
  6. Terminate the call.
  7. Verify SCO disconnected.
  8. Verify A2DP resumed.

Parameters:

Name Type Description Default
supported_audio_codecs Sequence[AudioCodec]

Audio codecs supported by REF device.

required
handle_audio_focus bool

Whether to enable audio focus handling.

False
Source code in navi/tests/functionality/coex_test.py
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
@navi_test_base.named_parameterized(
    cvsd=dict(
        supported_audio_codecs=[_AudioCodec.CVSD],
    ),
    msbc=dict(
        supported_audio_codecs=[_AudioCodec.CVSD, _AudioCodec.MSBC],
    ),
    lc3_swb=dict(
        supported_audio_codecs=[
            _AudioCodec.LC3_SWB,
            _AudioCodec.CVSD,
            _AudioCodec.MSBC,
        ],
    ),
    handle_audio_focus=dict(
        supported_audio_codecs=[
            _AudioCodec.LC3_SWB,
            _AudioCodec.CVSD,
            _AudioCodec.MSBC,
        ],
        handle_audio_focus=True,
    ),
)
async def test_point_to_point_ag_call_during_a2dp(
    self,
    supported_audio_codecs: collections.abc.Sequence[hfp.AudioCodec],
    handle_audio_focus: bool = False,
) -> None:
  """Tests making an outgoing phone call while A2DP is playing.

  Test steps:
    1. Setup HFP and A2DP connection.
    2. Play sine and check A2DP is playing.
    3. Place an outgoing call.
    4. Check A2DP is stopped.
    5. Verify SCO connected.
    6. Terminate the call.
    7. Verify SCO disconnected.
    8. Verify A2DP resumed.

  Args:
    supported_audio_codecs: Audio codecs supported by REF device.
    handle_audio_focus: Whether to enable audio focus handling.
  """
  self.ref = self.refs[0]

  self.logger.info("[DUT] Set audio focus to %s.", handle_audio_focus)
  self.dut.bt.setAudioAttributes(None, handle_audio_focus)

  self._setup_headset_device(
      hf_configuration=hfp_ext.make_hf_configuration(
          supported_hf_features=[hfp.HfFeature.CODEC_NEGOTIATION],
          supported_audio_codecs=supported_audio_codecs,
      ),
      a2dp_codecs=[a2dp_ext.A2dpCodec.SBC],
  )

  dut_hfp_cb = self.dut.bl4a.register_callback(_Module.HFP_AG)
  dut_a2dp_cb = self.dut.bl4a.register_callback(_Module.A2DP)
  dut_player_cb = self.dut.bl4a.register_callback(_Module.PLAYER)
  self.test_case_context.push(dut_hfp_cb)
  self.test_case_context.push(dut_a2dp_cb)
  self.test_case_context.push(dut_player_cb)

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

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

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

  self.logger.info("[DUT] Set repeat mode to all.")
  self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ALL)

  self.logger.info("[DUT] Start stream.")
  self.dut.bt.audioPlaySine()

  self.logger.info("[DUT] Check A2DP is playing.")
  await dut_a2dp_cb.wait_for_event(
      bl4a_api.A2dpPlayingStateChanged(
          address=self.ref.address, state=android_constants.A2dpState.PLAYING
      ),
  )

  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.")
  call = self.dut.bl4a.make_phone_call(
      _CALLER_NAME,
      _CALLER_NUMBER,
      constants.Direction.OUTGOING,
  )

  with call:
    self.logger.info("[DUT] Check A2DP is not playing.")
    await dut_a2dp_cb.wait_for_event(
        bl4a_api.A2dpPlayingStateChanged(
            address=self.ref.address,
            state=android_constants.A2dpState.NOT_PLAYING,
        ),
    )

    self.logger.info("[DUT] Wait for SCO connected.")
    await dut_hfp_cb.wait_for_event(
        _HfpAgAudioStateChange(
            address=self.ref.address, state=_ScoState.CONNECTED
        ),
    )

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

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

    sco_disconnected = asyncio.Event()
    sco_link.once(
        sco_link.EVENT_DISCONNECTION, lambda *_: sco_disconnected.set()
    )

    self.logger.info("[DUT] Terminate call.")
    call.close()

  self.logger.info("[DUT] Wait for SCO disconnected.")
  await dut_hfp_cb.wait_for_event(
      _HfpAgAudioStateChange(
          address=self.ref.address, state=_ScoState.DISCONNECTED
      ),
  )

  self.logger.info("[REF] Wait for SCO disconnected.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await sco_disconnected.wait()

  self.logger.info("[DUT] Wait for A2DP resume.")
  await dut_a2dp_cb.wait_for_event(
      bl4a_api.A2dpPlayingStateChanged(
          address=self.ref.address, state=android_constants.A2dpState.PLAYING
      ),
  )

  if handle_audio_focus:
    self.logger.info("[DUT] Wait for player resumed.")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=True),
    )

Bases: TwoDevicesTestBase

Tests for GATT Server role.

Source code in navi/tests/functionality/gatt_server_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
class GattServerTest(navi_test_base.TwoDevicesTestBase):
  """Tests for GATT Server role."""

  dut_name: str

  @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()

    # Use a unique name to avoid conflicts.
    self.dut_name = f"gatt_server_test_{uuid.uuid4().hex[:8]}"
    self.dut.bt.setName(self.dut_name)
    self.logger.info("dut_name: %s", self.dut.bt.getName())

  async def _setup_gatt_server(
      self, is_private: bool = False
  ) -> bl4a_api.GattServer:
    """Sets up a private GATT server on DUT."""
    dut_gatt_server = self.dut.bl4a.create_gatt_server()
    self.test_case_context.enter_context(dut_gatt_server)
    self.logger.info(
        "[DUT] Start advertising with Non-resolvable private address."
    )
    advertiser = await self.dut.bl4a.start_extended_advertising_set(
        bl4a_api.AdvertisingSetParameters(
            connectable=True,
            own_address_type=(
                android_constants.AddressTypeStatus.RANDOM_NON_RESOLVABLE
                if is_private
                else android_constants.AddressTypeStatus.RANDOM
            ),
        ),
        gatt_server=dut_gatt_server if is_private else None,
        advertising_data=bl4a_api.AdvertisingData(include_device_name=True),
        scan_response=bl4a_api.AdvertisingData(),
        periodic_advertising_parameters=None,
        periodic_advertising_data=None,
        duration=0,
        max_extended_advertising_events=0,
    )
    self.test_case_context.enter_context(advertiser)
    return dut_gatt_server

  @retry.retry_on_exception()
  async def _make_le_connection(self) -> device.Connection:
    """Connects to DUT over LE and returns the connection."""
    ref_dut_acl = await self.ref.device.connect(
        self.dut_name,
        transport=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_private_server_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())
    dut_gatt_server = await self._setup_gatt_server(is_private=True)

    self.logger.info("[DUT] Add a service.")
    await 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 device.Peer(ref_dut_acl) as peer:
      self.logger.info("[REF] Check services.")
      services = await peer.discover_services([core.UUID(service_uuid)])
      self.assertLen(services, 1)
      characteristics = await peer.discover_characteristics(
          [core.UUID(characteristic_uuid)], services[0]
      )
      self.assertLen(characteristics, 1)
      self.assertEqual(
          characteristics[0].properties, gatt.Characteristic.Properties.READ
      )

  async def test_private_server_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())
    dut_gatt_server = await self._setup_gatt_server(is_private=True)

    self.logger.info("[DUT] Add a service.")
    await 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 device.Peer(ref_dut_acl) as peer:
      services = await peer.discover_services([core.UUID(service_uuid)])
      self.assertLen(services, 1)
      characteristics = await peer.discover_characteristics(
          [core.UUID(characteristic_uuid)], services[0]
      )
      self.assertLen(characteristics, 1)
      characteristic = characteristics[0]

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

      read_request = await dut_gatt_server.wait_for_event(
          event=bl4a_api.GattCharacteristicReadRequest,
          predicate=lambda request: (
              request.characteristic_uuid == characteristic_uuid
          ),
      )
      expected_data = secrets.token_bytes(16)
      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)

  @navi_test_base.named_parameterized(
      with_response=True,
      without_response=False,
  )
  async def test_private_server_handle_characteristic_write_request(
      self, with_response: bool
  ) -> 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.

    Args:
      with_response: Whether to test write with response or without response. If
        True, test write with response; otherwise, test write without response.
    """

    # 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())
    dut_gatt_server = await self._setup_gatt_server(is_private=True)

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

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

    async with device.Peer(ref_dut_acl) as peer:
      services = await peer.discover_services([core.UUID(service_uuid)])
      self.assertLen(services, 1)
      characteristics = await peer.discover_characteristics(
          [core.UUID(characteristic_uuid)], services[0]
      )
      self.assertLen(characteristics, 1)
      characteristic = characteristics[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=with_response)
      )

      write_request = await 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.assertEqual(write_request.response_needed, with_response)

      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_private_server_handle_subscription(self) -> None:
    """Tests sending GATT notification / indication to REF.

    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())
    dut_gatt_server = await self._setup_gatt_server(is_private=True)

    self.logger.info("[DUT] Add a service.")
    await dut_gatt_server.add_service(
        bl4a_api.GattService(
            uuid=service_uuid,
            characteristics=[
                bl4a_api.GattCharacteristic(
                    uuid=characteristic_uuid,
                    properties=(
                        _Property.READ | _Property.NOTIFY | _Property.INDICATE
                    ),
                    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, 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 device.Peer(ref_dut_acl) as peer:
      target_services = peer.get_services_by_uuid(uuid=core.UUID(service_uuid))

      if not target_services:
        self.fail("Cannot find service.")

      ref_characteristics = peer.get_characteristics_by_uuid(
          core.UUID(characteristic_uuid),
          target_services[0],
      )

      if not ref_characteristics:
        self.fail("Cannot find characteristic.")

      ref_characteristic = ref_characteristics[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, prefer_notify=False
          )
      )

      self.logger.info("[DUT] Wait for CCCD write.")
      subscribe_request = await 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.")
      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.")
      dut_gatt_server.send_notification(
          address=self.ref.random_address,
          characteristic_handle=dut_characteristic.handle,
          # True for indication, False for notification. Currently rust
          # implementation only supports indication due to which notification
          # test fails.
          # TODO: Add NOTIFY (0x10) in rust/private_gatt
          confirm=True,
          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)

  async def test_eatt_connection_without_encryption(self) -> None:
    """Tests EATT connection without encryption should fail.

    Test steps:
      1. Start advertising on DUT.
      2. Connect to DUT over LE.
      3. Try to connect to EATT.
      4. Verify that EATT connection fails.
    """

    self.logger.info("[DUT] Start advertising.")
    advertiser = await self.dut.bl4a.start_extended_advertising_set(
        bl4a_api.AdvertisingSetParameters(
            connectable=True,
            own_address_type=(android_constants.AddressTypeStatus.RANDOM),
        ),
        advertising_data=bl4a_api.AdvertisingData(include_device_name=True),
    )
    self.test_case_context.enter_context(advertiser)

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

    self.logger.info("[REF] Try to connect to EATT.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      with self.assertRaises(l2cap.L2capError):
        await gatt_client.Client.connect_eatt(ref_dut_acl)

Tests EATT connection without encryption should fail.

Test steps
  1. Start advertising on DUT.
  2. Connect to DUT over LE.
  3. Try to connect to EATT.
  4. Verify that EATT connection fails.
Source code in navi/tests/functionality/gatt_server_test.py
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
async def test_eatt_connection_without_encryption(self) -> None:
  """Tests EATT connection without encryption should fail.

  Test steps:
    1. Start advertising on DUT.
    2. Connect to DUT over LE.
    3. Try to connect to EATT.
    4. Verify that EATT connection fails.
  """

  self.logger.info("[DUT] Start advertising.")
  advertiser = await self.dut.bl4a.start_extended_advertising_set(
      bl4a_api.AdvertisingSetParameters(
          connectable=True,
          own_address_type=(android_constants.AddressTypeStatus.RANDOM),
      ),
      advertising_data=bl4a_api.AdvertisingData(include_device_name=True),
  )
  self.test_case_context.enter_context(advertiser)

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

  self.logger.info("[REF] Try to connect to EATT.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    with self.assertRaises(l2cap.L2capError):
      await gatt_client.Client.connect_eatt(ref_dut_acl)

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/functionality/gatt_server_test.py
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
async def test_private_server_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())
  dut_gatt_server = await self._setup_gatt_server(is_private=True)

  self.logger.info("[DUT] Add a service.")
  await 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 device.Peer(ref_dut_acl) as peer:
    self.logger.info("[REF] Check services.")
    services = await peer.discover_services([core.UUID(service_uuid)])
    self.assertLen(services, 1)
    characteristics = await peer.discover_characteristics(
        [core.UUID(characteristic_uuid)], services[0]
    )
    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/functionality/gatt_server_test.py
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
async def test_private_server_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())
  dut_gatt_server = await self._setup_gatt_server(is_private=True)

  self.logger.info("[DUT] Add a service.")
  await 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 device.Peer(ref_dut_acl) as peer:
    services = await peer.discover_services([core.UUID(service_uuid)])
    self.assertLen(services, 1)
    characteristics = await peer.discover_characteristics(
        [core.UUID(characteristic_uuid)], services[0]
    )
    self.assertLen(characteristics, 1)
    characteristic = characteristics[0]

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

    read_request = await dut_gatt_server.wait_for_event(
        event=bl4a_api.GattCharacteristicReadRequest,
        predicate=lambda request: (
            request.characteristic_uuid == characteristic_uuid
        ),
    )
    expected_data = secrets.token_bytes(16)
    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.

Parameters:

Name Type Description Default
with_response bool

Whether to test write with response or without response. If True, test write with response; otherwise, test write without response.

required
Source code in navi/tests/functionality/gatt_server_test.py
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
@navi_test_base.named_parameterized(
    with_response=True,
    without_response=False,
)
async def test_private_server_handle_characteristic_write_request(
    self, with_response: bool
) -> 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.

  Args:
    with_response: Whether to test write with response or without response. If
      True, test write with response; otherwise, test write without response.
  """

  # 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())
  dut_gatt_server = await self._setup_gatt_server(is_private=True)

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

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

  async with device.Peer(ref_dut_acl) as peer:
    services = await peer.discover_services([core.UUID(service_uuid)])
    self.assertLen(services, 1)
    characteristics = await peer.discover_characteristics(
        [core.UUID(characteristic_uuid)], services[0]
    )
    self.assertLen(characteristics, 1)
    characteristic = characteristics[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=with_response)
    )

    write_request = await 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.assertEqual(write_request.response_needed, with_response)

    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 / indication to REF.

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/functionality/gatt_server_test.py
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
async def test_private_server_handle_subscription(self) -> None:
  """Tests sending GATT notification / indication to REF.

  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())
  dut_gatt_server = await self._setup_gatt_server(is_private=True)

  self.logger.info("[DUT] Add a service.")
  await dut_gatt_server.add_service(
      bl4a_api.GattService(
          uuid=service_uuid,
          characteristics=[
              bl4a_api.GattCharacteristic(
                  uuid=characteristic_uuid,
                  properties=(
                      _Property.READ | _Property.NOTIFY | _Property.INDICATE
                  ),
                  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, 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 device.Peer(ref_dut_acl) as peer:
    target_services = peer.get_services_by_uuid(uuid=core.UUID(service_uuid))

    if not target_services:
      self.fail("Cannot find service.")

    ref_characteristics = peer.get_characteristics_by_uuid(
        core.UUID(characteristic_uuid),
        target_services[0],
    )

    if not ref_characteristics:
      self.fail("Cannot find characteristic.")

    ref_characteristic = ref_characteristics[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, prefer_notify=False
        )
    )

    self.logger.info("[DUT] Wait for CCCD write.")
    subscribe_request = await 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.")
    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.")
    dut_gatt_server.send_notification(
        address=self.ref.random_address,
        characteristic_handle=dut_characteristic.handle,
        # True for indication, False for notification. Currently rust
        # implementation only supports indication due to which notification
        # test fails.
        # TODO: Add NOTIFY (0x10) in rust/private_gatt
        confirm=True,
        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: MultiDevicesTestBase

Tests LE Hearing Aid profile.

Source code in navi/tests/functionality/hap_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
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
class HapTest(navi_test_base.MultiDevicesTestBase):
  """Tests LE Hearing Aid profile."""

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

    if self.dut.device.is_emulator:
      # Force enable HAP client on the emulator.
      self.dut.shell(["setprop", _Property.HAP_CLIENT_ENABLED, "true"])
      self.dut.shell(
          ["setprop", _Property.CSIP_SET_COORDINATOR_ENABLED, "true"]
      )

    if self.dut.getprop(_Property.HAP_CLIENT_ENABLED) != "true":
      raise signals.TestAbortClass("HAP Client is not enabled on DUT.")

  def _setup_hap_servers(
      self,
      hearing_aid_type: _HaType = _HaType.MONAURAL_HEARING_AID,
      preset_synchronization_support: hap.PresetSynchronizationSupport = hap.PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED,
  ) -> None:
    features = hap.HearingAidFeatures(
        hearing_aid_type=hearing_aid_type,
        preset_synchronization_support=preset_synchronization_support,
        independent_presets=hap.IndependentPresets.IDENTICAL_PRESET_RECORD,
        dynamic_presets=hap.DynamicPresets.PRESET_RECORDS_MAY_CHANGE,
        writable_presets_support=hap.WritablePresetsSupport.WRITABLE_PRESET_RECORDS_SUPPORTED,
    )
    if hearing_aid_type == _HaType.BINAURAL_HEARING_AID:
      sirk = secrets.token_bytes(csip.SET_IDENTITY_RESOLVING_KEY_LENGTH)
      for ref in self.refs:
        ref.device.add_services([
            _HearingAccessService(
                device=ref.device,
                features=features,
                presets=[
                    copy.copy(_FOO_PRESET),
                    copy.copy(_BAR_PRESET),
                    copy.copy(_UNAVAILABLE_PRESET),
                ],
            ),
            cap.CommonAudioServiceService(
                csip.CoordinatedSetIdentificationService(
                    set_identity_resolving_key=sirk,
                    set_identity_resolving_key_type=csip.SirkType.PLAINTEXT,
                    coordinated_set_size=2,
                )
            ),
        ])
    else:
      self.refs[0].device.add_service(
          _HearingAccessService(
              device=self.refs[0].device,
              features=features,
              presets=[
                  copy.copy(_FOO_PRESET),
                  copy.copy(_BAR_PRESET),
                  copy.copy(_UNAVAILABLE_PRESET),
              ],
          )
      )

  async def _setup_connections(self, hearing_aid_type: _HaType) -> None:
    with self.dut.bl4a.register_callback(_Module.HAP_CLIENT) as dut_hap_cb:
      for i, ref in enumerate(
          self.refs
          if hearing_aid_type == _HaType.BINAURAL_HEARING_AID
          else [self.refs[0]]
      ):
        self.logger.info("[DUT] Connect to REF-%d", i)
        await self.le_connect_and_pair(hci.OwnAddressType.RANDOM, ref)
        self.dut.bt.setHapConnectionPolicy(
            ref.random_address, android_constants.ConnectionPolicy.ALLOWED
        )
        self.logger.info("[DUT] Wait for HAP connected to REF-%d", i)
        await dut_hap_cb.wait_for_event(
            bl4a_api.ProfileConnectionStateChanged(
                address=ref.random_address,
                state=android_constants.ConnectionState.CONNECTED,
            ),
            timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
        )
        self.logger.info("[DUT] Set MTU to 517 for REF-%d", i)
        gatt_client = await self.dut.bl4a.connect_gatt_client(
            ref.random_address, transport=android_constants.Transport.LE
        )
        await gatt_client.request_mtu(517)

  @navi_test_base.named_parameterized(
      ("binaural", _HaType.BINAURAL_HEARING_AID),
      ("monaural", _HaType.MONAURAL_HEARING_AID),
  )
  async def test_remove_preset(self, hearing_aid_type: _HaType) -> None:
    """Test removing a preset from the REF.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Remove a preset from the REF.
      3. Verify the DUT receives the update and the preset info is changed.

    Args:
      hearing_aid_type: Whether the test is for binaural or monaural.
    """
    self._setup_hap_servers(hearing_aid_type)
    await self._setup_connections(hearing_aid_type)

    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

    dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
    self.test_case_context.enter_context(dut_hap_callback)

    self.logger.info("[REF] Delete preset")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await has.delete_preset(index=_UNAVAILABLE_PRESET.index)

    self.logger.info("[DUT] Wait for preset info changed")
    await dut_hap_callback.wait_for_event(
        bl4a_api.PresetInfoChanged(
            address=self.refs[0].random_address,
            reason=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.assertDictEqual(
        {rec.index: rec.name for rec in has.preset_records.values()},
        self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
    )

  @navi_test_base.named_parameterized(
      ("binaural", _HaType.BINAURAL_HEARING_AID),
      ("monaural", _HaType.MONAURAL_HEARING_AID),
  )
  async def test_add_preset(self, hearing_aid_type: _HaType) -> None:
    """Test adding a preset from the REF.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Add a preset from the REF.
      3. Verify the DUT receives the update and the preset info is changed.

    Args:
      hearing_aid_type: Whether the test is for binaural or monaural.
    """
    self._setup_hap_servers(hearing_aid_type)
    await self._setup_connections(hearing_aid_type)

    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

    dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
    self.test_case_context.enter_context(dut_hap_callback)

    self.logger.info("[REF] Add preset")
    added_preset = hap.PresetRecord(_BAR_PRESET.index + 3, "added_preset")
    has.preset_records[added_preset.index] = added_preset
    await has.generic_update(
        hap.PresetChangedOperation(
            hap.PresetChangedOperation.ChangeId.GENERIC_UPDATE,
            hap.PresetChangedOperation.Generic(_BAR_PRESET.index, added_preset),
        )
    )

    self.logger.info("[DUT] Wait for preset info changed")
    await dut_hap_callback.wait_for_event(
        bl4a_api.PresetInfoChanged(
            address=self.refs[0].random_address,
            reason=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.assertDictEqual(
        {rec.index: rec.name for rec in has.preset_records.values()},
        self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
    )

  @navi_test_base.named_parameterized(
      ("binaural", _HaType.BINAURAL_HEARING_AID),
      ("monaural", _HaType.MONAURAL_HEARING_AID),
  )
  async def test_update_preset(self, hearing_aid_type: _HaType) -> None:
    """Test updating a preset from the REF.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Update a preset from the REF.
      3. Verify the DUT receives the update and the preset info is changed.

    Args:
      hearing_aid_type: Whether the test is for binaural or monaural.
    """

    self._setup_hap_servers(hearing_aid_type)
    await self._setup_connections(hearing_aid_type)

    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

    dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
    self.test_case_context.enter_context(dut_hap_callback)

    self.logger.info("[REF] Update preset")
    has.preset_records[_FOO_PRESET.index].name = "Very nice name"
    await has.generic_update(
        hap.PresetChangedOperation(
            hap.PresetChangedOperation.ChangeId.GENERIC_UPDATE,
            hap.PresetChangedOperation.Generic(
                _FOO_PRESET.index, has.preset_records[_FOO_PRESET.index]
            ),
        )
    )

    self.logger.info("[DUT] Wait for preset info changed")
    await dut_hap_callback.wait_for_event(
        bl4a_api.PresetInfoChanged(
            address=self.refs[0].random_address,
            reason=mock.ANY,
        ),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.assertDictEqual(
        {rec.index: rec.name for rec in has.preset_records.values()},
        self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
    )

  @navi_test_base.named_parameterized(
      ("binaural", _HaType.BINAURAL_HEARING_AID),
      ("monaural", _HaType.MONAURAL_HEARING_AID),
  )
  async def test_set_active_preset(self, hearing_aid_type: _HaType) -> None:
    """Test setting a preset as active, the active preset should be changed.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Set a preset as active.
      3. Verify the active preset is changed.

    Args:
      hearing_aid_type: Whether the test is for binaural or monaural.
    """
    self._setup_hap_servers(hearing_aid_type)
    await self._setup_connections(hearing_aid_type)

    for preset in (_BAR_PRESET, _FOO_PRESET):
      self.logger.info("[DUT] Set active preset to %d", preset.index)
      self.dut.bt.selectHapPreset(self.refs[0].random_address, preset.index)

      self.logger.info("[REF] Verify active preset")
      has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await has.wait_for_active_preset_index(preset.index)

  async def test_set_active_preset_for_group(self) -> None:
    self._setup_hap_servers(hearing_aid_type=_HaType.BINAURAL_HEARING_AID)
    await self._setup_connections(hearing_aid_type=_HaType.BINAURAL_HEARING_AID)

    self.logger.info("[DUT] Set active preset")
    group_id = self.dut.bt.getHapGroup(self.refs[0].random_address)
    self.assertGreater(group_id, 0, "Group ID is not greater than 0")
    self.dut.bt.selectHapPresetForGroup(group_id, _BAR_PRESET.index)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for i, ref in enumerate(self.refs):
        self.logger.info("[REF-%d] Verify active preset", i)
        has = _get_service_from_device(ref.device, _HearingAccessService)
        await has.wait_for_active_preset_index(_BAR_PRESET.index)

  async def test_set_non_existing_preset_as_active(self) -> None:
    """Test setting a non-existing preset as available, the preset should be ignored.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Notify a non-existing preset to the DUT via the REF.
      3. Verify the DUT ignores the notification and the active preset is not
         changed.
    """
    self._setup_hap_servers(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)
    await self._setup_connections(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)

    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
    self.logger.info("[REF] Notify active preset to non existing index")
    has.active_preset_index = 79
    await has.notify_active_preset()

    # Operation should be ignored, so the active preset should not be changed.
    self.assertEqual(
        self.dut.bt.getActiveHapPresetIndex(self.refs[0].random_address),
        _FOO_PRESET.index,
    )

  async def test_set_non_existing_preset_as_available(self) -> None:
    """Test setting a non-existing preset as available should not crash.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Notify a non-existing preset to the DUT via the REF.
      3. Verify the DUT ignores the notification and the active preset is not
         changed.
    """
    self._setup_hap_servers(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)
    await self._setup_connections(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)

    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
    self.logger.info("[REF] Notify available preset to non existing index")
    await has.generic_update(hap.PresetChangedOperationAvailable(79))

    # Not related, but make an RPC call to make sure BT is still alive.
    self.assertEqual(
        self.dut.bt.getActiveHapPresetIndex(self.refs[0].random_address),
        _FOO_PRESET.index,
    )

  async def test_synchronize_operation_failed_when_selecting_preset_can_recover(
      self,
  ) -> None:
    """Test synchronize operation failed when selecting preset can recover.

    Test Steps:
      1. Setup HAP servers and connections.
      2. Set a preset as active.
      3. Verify the active preset is changed.
      4. Set a preset as active again with sync enabled.
      5. Verify the active preset is changed.
    """
    self._setup_hap_servers(
        hearing_aid_type=_HaType.BINAURAL_HEARING_AID,
        preset_synchronization_support=hap.PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED,
    )
    await self._setup_connections(hearing_aid_type=_HaType.BINAURAL_HEARING_AID)

    ha_services = [
        _get_service_from_device(ref.device, _HearingAccessService)
        for ref in self.refs
    ]
    self.assertEqual(ha_services[0].active_preset_index, _FOO_PRESET.index)
    self.assertEqual(ha_services[1].active_preset_index, _FOO_PRESET.index)

    self.logger.info("[DUT] Set active preset for group to BAR preset.")
    group_id = self.dut.bt.getHapGroup(self.refs[0].random_address)
    self.assertGreater(group_id, 0, "Group ID is not greater than 0")
    self.dut.bt.selectHapPresetForGroup(group_id, _BAR_PRESET.index)

    self.logger.info("Wait for 11 seconds to let the operation timeout.")
    await asyncio.sleep(11)

    # As expected, only left preset has been updated
    self.assertEqual(ha_services[0].active_preset_index, _BAR_PRESET.index)
    self.assertEqual(ha_services[1].active_preset_index, _FOO_PRESET.index)

    self.logger.info("[REF] Enable preset synchronization")
    ha_services[0].other_server_in_binaural_set = ha_services[1]
    ha_services[1].other_server_in_binaural_set = ha_services[0]

    self.logger.info("[DUT] Set active preset for group again.")
    self.dut.bt.selectHapPresetForGroup(group_id, _BAR_PRESET.index)
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for i, has in enumerate(ha_services):
        self.logger.info("[REF-%d] Verify active preset", i)
        await has.wait_for_active_preset_index(_BAR_PRESET.index)

Test adding a preset from the REF.

Test Steps
  1. Setup HAP servers and connections.
  2. Add a preset from the REF.
  3. Verify the DUT receives the update and the preset info is changed.

Parameters:

Name Type Description Default
hearing_aid_type _HaType

Whether the test is for binaural or monaural.

required
Source code in navi/tests/functionality/hap_test.py
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
@navi_test_base.named_parameterized(
    ("binaural", _HaType.BINAURAL_HEARING_AID),
    ("monaural", _HaType.MONAURAL_HEARING_AID),
)
async def test_add_preset(self, hearing_aid_type: _HaType) -> None:
  """Test adding a preset from the REF.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Add a preset from the REF.
    3. Verify the DUT receives the update and the preset info is changed.

  Args:
    hearing_aid_type: Whether the test is for binaural or monaural.
  """
  self._setup_hap_servers(hearing_aid_type)
  await self._setup_connections(hearing_aid_type)

  has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

  dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
  self.test_case_context.enter_context(dut_hap_callback)

  self.logger.info("[REF] Add preset")
  added_preset = hap.PresetRecord(_BAR_PRESET.index + 3, "added_preset")
  has.preset_records[added_preset.index] = added_preset
  await has.generic_update(
      hap.PresetChangedOperation(
          hap.PresetChangedOperation.ChangeId.GENERIC_UPDATE,
          hap.PresetChangedOperation.Generic(_BAR_PRESET.index, added_preset),
      )
  )

  self.logger.info("[DUT] Wait for preset info changed")
  await dut_hap_callback.wait_for_event(
      bl4a_api.PresetInfoChanged(
          address=self.refs[0].random_address,
          reason=mock.ANY,
      ),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  self.assertDictEqual(
      {rec.index: rec.name for rec in has.preset_records.values()},
      self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
  )

Test removing a preset from the REF.

Test Steps
  1. Setup HAP servers and connections.
  2. Remove a preset from the REF.
  3. Verify the DUT receives the update and the preset info is changed.

Parameters:

Name Type Description Default
hearing_aid_type _HaType

Whether the test is for binaural or monaural.

required
Source code in navi/tests/functionality/hap_test.py
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
@navi_test_base.named_parameterized(
    ("binaural", _HaType.BINAURAL_HEARING_AID),
    ("monaural", _HaType.MONAURAL_HEARING_AID),
)
async def test_remove_preset(self, hearing_aid_type: _HaType) -> None:
  """Test removing a preset from the REF.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Remove a preset from the REF.
    3. Verify the DUT receives the update and the preset info is changed.

  Args:
    hearing_aid_type: Whether the test is for binaural or monaural.
  """
  self._setup_hap_servers(hearing_aid_type)
  await self._setup_connections(hearing_aid_type)

  has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

  dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
  self.test_case_context.enter_context(dut_hap_callback)

  self.logger.info("[REF] Delete preset")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    await has.delete_preset(index=_UNAVAILABLE_PRESET.index)

  self.logger.info("[DUT] Wait for preset info changed")
  await dut_hap_callback.wait_for_event(
      bl4a_api.PresetInfoChanged(
          address=self.refs[0].random_address,
          reason=mock.ANY,
      ),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  self.assertDictEqual(
      {rec.index: rec.name for rec in has.preset_records.values()},
      self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
  )

Test setting a preset as active, the active preset should be changed.

Test Steps
  1. Setup HAP servers and connections.
  2. Set a preset as active.
  3. Verify the active preset is changed.

Parameters:

Name Type Description Default
hearing_aid_type _HaType

Whether the test is for binaural or monaural.

required
Source code in navi/tests/functionality/hap_test.py
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
@navi_test_base.named_parameterized(
    ("binaural", _HaType.BINAURAL_HEARING_AID),
    ("monaural", _HaType.MONAURAL_HEARING_AID),
)
async def test_set_active_preset(self, hearing_aid_type: _HaType) -> None:
  """Test setting a preset as active, the active preset should be changed.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Set a preset as active.
    3. Verify the active preset is changed.

  Args:
    hearing_aid_type: Whether the test is for binaural or monaural.
  """
  self._setup_hap_servers(hearing_aid_type)
  await self._setup_connections(hearing_aid_type)

  for preset in (_BAR_PRESET, _FOO_PRESET):
    self.logger.info("[DUT] Set active preset to %d", preset.index)
    self.dut.bt.selectHapPreset(self.refs[0].random_address, preset.index)

    self.logger.info("[REF] Verify active preset")
    has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await has.wait_for_active_preset_index(preset.index)

Test setting a non-existing preset as available, the preset should be ignored.

Test Steps
  1. Setup HAP servers and connections.
  2. Notify a non-existing preset to the DUT via the REF.
  3. Verify the DUT ignores the notification and the active preset is not changed.
Source code in navi/tests/functionality/hap_test.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
async def test_set_non_existing_preset_as_active(self) -> None:
  """Test setting a non-existing preset as available, the preset should be ignored.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Notify a non-existing preset to the DUT via the REF.
    3. Verify the DUT ignores the notification and the active preset is not
       changed.
  """
  self._setup_hap_servers(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)
  await self._setup_connections(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)

  has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
  self.logger.info("[REF] Notify active preset to non existing index")
  has.active_preset_index = 79
  await has.notify_active_preset()

  # Operation should be ignored, so the active preset should not be changed.
  self.assertEqual(
      self.dut.bt.getActiveHapPresetIndex(self.refs[0].random_address),
      _FOO_PRESET.index,
  )

Test setting a non-existing preset as available should not crash.

Test Steps
  1. Setup HAP servers and connections.
  2. Notify a non-existing preset to the DUT via the REF.
  3. Verify the DUT ignores the notification and the active preset is not changed.
Source code in navi/tests/functionality/hap_test.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
async def test_set_non_existing_preset_as_available(self) -> None:
  """Test setting a non-existing preset as available should not crash.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Notify a non-existing preset to the DUT via the REF.
    3. Verify the DUT ignores the notification and the active preset is not
       changed.
  """
  self._setup_hap_servers(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)
  await self._setup_connections(hearing_aid_type=_HaType.MONAURAL_HEARING_AID)

  has = _get_service_from_device(self.refs[0].device, _HearingAccessService)
  self.logger.info("[REF] Notify available preset to non existing index")
  await has.generic_update(hap.PresetChangedOperationAvailable(79))

  # Not related, but make an RPC call to make sure BT is still alive.
  self.assertEqual(
      self.dut.bt.getActiveHapPresetIndex(self.refs[0].random_address),
      _FOO_PRESET.index,
  )

Test synchronize operation failed when selecting preset can recover.

Test Steps
  1. Setup HAP servers and connections.
  2. Set a preset as active.
  3. Verify the active preset is changed.
  4. Set a preset as active again with sync enabled.
  5. Verify the active preset is changed.
Source code in navi/tests/functionality/hap_test.py
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
async def test_synchronize_operation_failed_when_selecting_preset_can_recover(
    self,
) -> None:
  """Test synchronize operation failed when selecting preset can recover.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Set a preset as active.
    3. Verify the active preset is changed.
    4. Set a preset as active again with sync enabled.
    5. Verify the active preset is changed.
  """
  self._setup_hap_servers(
      hearing_aid_type=_HaType.BINAURAL_HEARING_AID,
      preset_synchronization_support=hap.PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED,
  )
  await self._setup_connections(hearing_aid_type=_HaType.BINAURAL_HEARING_AID)

  ha_services = [
      _get_service_from_device(ref.device, _HearingAccessService)
      for ref in self.refs
  ]
  self.assertEqual(ha_services[0].active_preset_index, _FOO_PRESET.index)
  self.assertEqual(ha_services[1].active_preset_index, _FOO_PRESET.index)

  self.logger.info("[DUT] Set active preset for group to BAR preset.")
  group_id = self.dut.bt.getHapGroup(self.refs[0].random_address)
  self.assertGreater(group_id, 0, "Group ID is not greater than 0")
  self.dut.bt.selectHapPresetForGroup(group_id, _BAR_PRESET.index)

  self.logger.info("Wait for 11 seconds to let the operation timeout.")
  await asyncio.sleep(11)

  # As expected, only left preset has been updated
  self.assertEqual(ha_services[0].active_preset_index, _BAR_PRESET.index)
  self.assertEqual(ha_services[1].active_preset_index, _FOO_PRESET.index)

  self.logger.info("[REF] Enable preset synchronization")
  ha_services[0].other_server_in_binaural_set = ha_services[1]
  ha_services[1].other_server_in_binaural_set = ha_services[0]

  self.logger.info("[DUT] Set active preset for group again.")
  self.dut.bt.selectHapPresetForGroup(group_id, _BAR_PRESET.index)
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    for i, has in enumerate(ha_services):
      self.logger.info("[REF-%d] Verify active preset", i)
      await has.wait_for_active_preset_index(_BAR_PRESET.index)

Test updating a preset from the REF.

Test Steps
  1. Setup HAP servers and connections.
  2. Update a preset from the REF.
  3. Verify the DUT receives the update and the preset info is changed.

Parameters:

Name Type Description Default
hearing_aid_type _HaType

Whether the test is for binaural or monaural.

required
Source code in navi/tests/functionality/hap_test.py
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
@navi_test_base.named_parameterized(
    ("binaural", _HaType.BINAURAL_HEARING_AID),
    ("monaural", _HaType.MONAURAL_HEARING_AID),
)
async def test_update_preset(self, hearing_aid_type: _HaType) -> None:
  """Test updating a preset from the REF.

  Test Steps:
    1. Setup HAP servers and connections.
    2. Update a preset from the REF.
    3. Verify the DUT receives the update and the preset info is changed.

  Args:
    hearing_aid_type: Whether the test is for binaural or monaural.
  """

  self._setup_hap_servers(hearing_aid_type)
  await self._setup_connections(hearing_aid_type)

  has = _get_service_from_device(self.refs[0].device, _HearingAccessService)

  dut_hap_callback = self.dut.bl4a.register_callback(_Module.HAP_CLIENT)
  self.test_case_context.enter_context(dut_hap_callback)

  self.logger.info("[REF] Update preset")
  has.preset_records[_FOO_PRESET.index].name = "Very nice name"
  await has.generic_update(
      hap.PresetChangedOperation(
          hap.PresetChangedOperation.ChangeId.GENERIC_UPDATE,
          hap.PresetChangedOperation.Generic(
              _FOO_PRESET.index, has.preset_records[_FOO_PRESET.index]
          ),
      )
  )

  self.logger.info("[DUT] Wait for preset info changed")
  await dut_hap_callback.wait_for_event(
      bl4a_api.PresetInfoChanged(
          address=self.refs[0].random_address,
          reason=mock.ANY,
      ),
      timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
  )
  self.assertDictEqual(
      {rec.index: rec.name for rec in has.preset_records.values()},
      self.dut.bl4a.get_all_hap_preset_info(self.refs[0].random_address),
  )

Bases: TwoDevicesTestBase

Source code in navi/tests/functionality/hfp_ag_test.py
 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
class HfpAgTest(navi_test_base.TwoDevicesTestBase):

  @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()

  async def _pair_and_connect_with_hfp_server_on_ref(self) -> None:
    """Setup HFP connection establishment right after a pairing session."""
    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=hfp_ext.make_hf_configuration(),
      )

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

      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 _find_or_connect_acl_from_ref(
      self, dut_address: str
  ) -> bumble_device.Connection:
    if not (
        dut_ref_acl := self.ref.device.find_connection_by_bd_addr(
            hci.Address(dut_address)
        )
    ):
      dut_ref_acl = await self.ref.device.connect(
          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()
    return dut_ref_acl

  async def _connect_hfp_from_dut(self, ref_address: str) -> None:
    """Initiates connection(HFP) from DUT to REF."""
    self.logger.info("[DUT] Initiating HFP connection.")
    self.dut.bt.connect(ref_address)

  async def _connect_hfp_from_ref(
      self, dut_ref_acl: bumble_device.Connection
  ) -> None:
    """Initiates HFP connection from REF to DUT."""
    self.logger.info("[REF] Initiating HFP connection.")
    # If ACL connection is not established, establish it.
    if dut_ref_acl is None:
      dut_ref_acl = await self._find_or_connect_acl_from_ref(self.dut.address)

    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.")
    multiplexer = await rfcomm.Client(dut_ref_acl).start()

    self.logger.info("[REF] Open RFCOMM DLC.")
    dlc = await multiplexer.open_dlc(rfcomm_channel)

    self.logger.info("[REF] Establish SLC.")
    ref_hfp_protocol = hfp_ext.HfProtocol(dlc, hfp_ext.make_hf_configuration())
    await ref_hfp_protocol.initiate_slc()

  async def test_paired_connect_hfp_ag_simultaneous(self) -> None:
    """Tests HFP connection establishment with simultaneous connection.

    Test steps:
      1. Setup pairing between DUT and REF.
      2. Terminate ACL connection.
      3. Setup ACL connection from REF.
      4. Trigger HFP connection from DUT and REF at same time.
      5. Wait HFP connected on DUT.
      6. Disconnect from DUT.
      7. Wait HFP disconnected on DUT.

    Test Result:
      AOSP has the ability to handle HFP Connection Collision. Even in
      conflicting scenarios, HFP must eventually connect successfully.
    """
    with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb:
      # Step 1: Setup pairing and initial hfp connection
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[DUT] Setup pairing and initial HFP connection.",
      ):
        await self._pair_and_connect_with_hfp_server_on_ref()

      # Step 2: Terminate ACL connection
      await self.disconnect_with_check(
          self.ref.address, android_constants.Transport.CLASSIC
      )

      # Step 3: Setup ACL connection from REF
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg="[REF] Find or connect ACL connection from DUT.",
      ):
        dut_ref_acl = await self._find_or_connect_acl_from_ref(self.dut.address)

      # Step 4: Trigger connection from DUT and REF at same time
      self.logger.info("[DUT & REF] Triggering simultaneous HFP connection.")

      # Use asyncio.gather to run both connection attempts concurrently
      try:
        await asyncio.wait_for(
            asyncio.gather(
                self._connect_hfp_from_dut(self.ref.address),
                self._connect_hfp_from_ref(dut_ref_acl),
            ),
            timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
        )
      # Raise error from REF side for HFP connection collision.
      # This is to avoid the test case being ignored due to the DUT side
      # will check HFP connection successfully.
      except (core.BaseBumbleError, TimeoutError):
        self.logger.warning(
            "[REF & DUT] Simultaneous HFP connection exception.",
            stack_info=True,
        )

      # Step 5: Wait for HFP to be connected on DUT
      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,
      )

      # Step 6: Disconnect from DUT
      self.logger.info("[DUT] Disconnect.")
      self.dut.bt.disconnect(self.ref.address)

      # Step 7: Wait for HFP to be disconnected on DUT
      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 _pair_and_connect_with_custom_hfp_server_on_ref(
      self,
      hfp_protocol_class: type[hfp_ext.HfProtocol],
  ) -> None:
    """Setup HFP connection with a custom HF protocol."""
    with (self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb,):
      hfp_protocol_class.setup_server(
          self.ref.device,
          sdp_handle=_HFP_SDP_HANDLE,
          configuration=hfp_ext.make_hf_configuration(
              supported_hf_features=[hfp.HfFeature.CODEC_NEGOTIATION],
              supported_audio_codecs=[
                  _AudioCodec.CVSD,
                  _AudioCodec.MSBC,
                  _AudioCodec.LC3_SWB,
              ],
          ),
      )

      self.logger.info(
          "[DUT] Connect and pair REF with %s.", hfp_protocol_class.__name__
      )
      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_sco_codec_negotiation_timeout(self) -> None:
    """Tests DUT handling of SCO codec negotiation timeout.

    Test steps:
      1. DUT and REF pair and connect HFP.
      2. DUT initiates an outgoing call, starting SCO connection.
      3. REF (custom HFP HF) does not reply to codec negotiation.
      4. DUT experiences codec negotiation timeout.
      5. Verify communication device falls back to non BT SCO device.

    Test Result:
      DUT should gracefully handle the SCO codec negotiation timeout.
    """

    # Step 1: DUT and REF pair and connect HFP.
    self.logger.info(
        "[DUT] Setup pairing and initial HFP connection with no reply HF."
    )
    await self._pair_and_connect_with_custom_hfp_server_on_ref(
        NoReplyCodecNegotiationHfProtocol
    )

    # Step 2: DUT initiates an outgoing call, starting SCO connection.
    self.logger.info("[DUT] Initiating outgoing call to trigger SCO.")

    with self.dut.bl4a.register_callback(
        bl4a_api.Module.HFP_AG
    ) as dut_hfp_ag_cb:
      with self.dut.bl4a.make_phone_call(
          caller_name=_CALLER_NAME,
          caller_number=_CALLER_NUMBER,
          direction=constants.Direction.OUTGOING,
      ):
        # Step 3: REF (custom HFP HF) does not reply to codec negotiation.
        # Step 4: DUT experiences codec negotiation timeout.

        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] Waiting for SCO negotiation to timeout.")
        async with self.assert_timeout(
            delay=5.0,
            msg="SCO audio should not become active due to timeout.",
        ):
          await dut_hfp_ag_cb.wait_for_event(
              bl4a_api.HfpAgAudioStateChanged(
                  address=self.ref.address,
                  state=android_constants.ScoState.CONNECTED,
              )
          )
        self.logger.info("[DUT] SCO codec negotiation timeout, as expected.")

        # Step 5: Verify communication device falls back to non BT SCO device.
        # After SCO codec negotiation timeout, Bluetooth notifies Audio of the
        # SCO connection failure, prompting Audio to switch the communication
        # device from SCO to the default device.
        self.logger.info(
            "[DUT] Check communication device fallback to non BT SCO device."
        )
        info_dict = self.dut.bt.getCommunicationDevice()
        if not info_dict:
          self.fail("Communication device is empty.")
        device = bl4a_api.AudioDeviceInfo.from_mapping(info_dict)
        self.logger.info("[DUT] Current communication device: %s", device)
        self.assertNotEqual(
            device.device_type,
            android_constants.AudioDeviceType.BLUETOOTH_SCO,
            "Communication device should not the Bluetooth SCO.",
        )

Tests HFP connection establishment with simultaneous connection.

Test steps
  1. Setup pairing between DUT and REF.
  2. Terminate ACL connection.
  3. Setup ACL connection from REF.
  4. Trigger HFP connection from DUT and REF at same time.
  5. Wait HFP connected on DUT.
  6. Disconnect from DUT.
  7. Wait HFP disconnected on DUT.
Test Result

AOSP has the ability to handle HFP Connection Collision. Even in conflicting scenarios, HFP must eventually connect successfully.

Source code in navi/tests/functionality/hfp_ag_test.py
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
async def test_paired_connect_hfp_ag_simultaneous(self) -> None:
  """Tests HFP connection establishment with simultaneous connection.

  Test steps:
    1. Setup pairing between DUT and REF.
    2. Terminate ACL connection.
    3. Setup ACL connection from REF.
    4. Trigger HFP connection from DUT and REF at same time.
    5. Wait HFP connected on DUT.
    6. Disconnect from DUT.
    7. Wait HFP disconnected on DUT.

  Test Result:
    AOSP has the ability to handle HFP Connection Collision. Even in
    conflicting scenarios, HFP must eventually connect successfully.
  """
  with self.dut.bl4a.register_callback(_Module.HFP_AG) as dut_cb:
    # Step 1: Setup pairing and initial hfp connection
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[DUT] Setup pairing and initial HFP connection.",
    ):
      await self._pair_and_connect_with_hfp_server_on_ref()

    # Step 2: Terminate ACL connection
    await self.disconnect_with_check(
        self.ref.address, android_constants.Transport.CLASSIC
    )

    # Step 3: Setup ACL connection from REF
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg="[REF] Find or connect ACL connection from DUT.",
    ):
      dut_ref_acl = await self._find_or_connect_acl_from_ref(self.dut.address)

    # Step 4: Trigger connection from DUT and REF at same time
    self.logger.info("[DUT & REF] Triggering simultaneous HFP connection.")

    # Use asyncio.gather to run both connection attempts concurrently
    try:
      await asyncio.wait_for(
          asyncio.gather(
              self._connect_hfp_from_dut(self.ref.address),
              self._connect_hfp_from_ref(dut_ref_acl),
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
    # Raise error from REF side for HFP connection collision.
    # This is to avoid the test case being ignored due to the DUT side
    # will check HFP connection successfully.
    except (core.BaseBumbleError, TimeoutError):
      self.logger.warning(
          "[REF & DUT] Simultaneous HFP connection exception.",
          stack_info=True,
      )

    # Step 5: Wait for HFP to be connected on DUT
    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,
    )

    # Step 6: Disconnect from DUT
    self.logger.info("[DUT] Disconnect.")
    self.dut.bt.disconnect(self.ref.address)

    # Step 7: Wait for HFP to be disconnected on DUT
    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 DUT handling of SCO codec negotiation timeout.

Test steps
  1. DUT and REF pair and connect HFP.
  2. DUT initiates an outgoing call, starting SCO connection.
  3. REF (custom HFP HF) does not reply to codec negotiation.
  4. DUT experiences codec negotiation timeout.
  5. Verify communication device falls back to non BT SCO device.
Test Result

DUT should gracefully handle the SCO codec negotiation timeout.

Source code in navi/tests/functionality/hfp_ag_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
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
async def test_sco_codec_negotiation_timeout(self) -> None:
  """Tests DUT handling of SCO codec negotiation timeout.

  Test steps:
    1. DUT and REF pair and connect HFP.
    2. DUT initiates an outgoing call, starting SCO connection.
    3. REF (custom HFP HF) does not reply to codec negotiation.
    4. DUT experiences codec negotiation timeout.
    5. Verify communication device falls back to non BT SCO device.

  Test Result:
    DUT should gracefully handle the SCO codec negotiation timeout.
  """

  # Step 1: DUT and REF pair and connect HFP.
  self.logger.info(
      "[DUT] Setup pairing and initial HFP connection with no reply HF."
  )
  await self._pair_and_connect_with_custom_hfp_server_on_ref(
      NoReplyCodecNegotiationHfProtocol
  )

  # Step 2: DUT initiates an outgoing call, starting SCO connection.
  self.logger.info("[DUT] Initiating outgoing call to trigger SCO.")

  with self.dut.bl4a.register_callback(
      bl4a_api.Module.HFP_AG
  ) as dut_hfp_ag_cb:
    with self.dut.bl4a.make_phone_call(
        caller_name=_CALLER_NAME,
        caller_number=_CALLER_NUMBER,
        direction=constants.Direction.OUTGOING,
    ):
      # Step 3: REF (custom HFP HF) does not reply to codec negotiation.
      # Step 4: DUT experiences codec negotiation timeout.

      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] Waiting for SCO negotiation to timeout.")
      async with self.assert_timeout(
          delay=5.0,
          msg="SCO audio should not become active due to timeout.",
      ):
        await dut_hfp_ag_cb.wait_for_event(
            bl4a_api.HfpAgAudioStateChanged(
                address=self.ref.address,
                state=android_constants.ScoState.CONNECTED,
            )
        )
      self.logger.info("[DUT] SCO codec negotiation timeout, as expected.")

      # Step 5: Verify communication device falls back to non BT SCO device.
      # After SCO codec negotiation timeout, Bluetooth notifies Audio of the
      # SCO connection failure, prompting Audio to switch the communication
      # device from SCO to the default device.
      self.logger.info(
          "[DUT] Check communication device fallback to non BT SCO device."
      )
      info_dict = self.dut.bt.getCommunicationDevice()
      if not info_dict:
        self.fail("Communication device is empty.")
      device = bl4a_api.AudioDeviceInfo.from_mapping(info_dict)
      self.logger.info("[DUT] Current communication device: %s", device)
      self.assertNotEqual(
          device.device_type,
          android_constants.AudioDeviceType.BLUETOOTH_SCO,
          "Communication device should not the Bluetooth SCO.",
      )

Bases: TwoDevicesTestBase

Source code in navi/tests/functionality/hid_headtracker_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
class HidHeadtrackerTest(navi_test_base.TwoDevicesTestBase):
  ref_headtracker_service: gatt.Service
  ref_headtracker_report_characteristic: gatt.Characteristic

  def _setup_lea_services(self) -> None:
    self.ref.device.add_service(
        pacs_ext.make_pacs(
            source_pacs=[
                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_7500_US_SUPPORTED
                            | 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,
                    ),
                ),
                pacs.PacRecord(
                    coding_format=hci.CodingFormat(
                        codec_id=hci.CodecID.VENDOR_SPECIFIC,
                        company_id=_VENDOR_COMPANY_ID_GOOGLE,
                        vendor_specific_codec_id=0x0002,
                    ),
                    codec_specific_capabilities=bap.CodecSpecificCapabilities(
                        supported_sampling_frequencies=bap.SupportedSamplingFrequency.FREQ_48000,
                        supported_frame_durations=(
                            bap.SupportedFrameDuration.DURATION_7500_US_SUPPORTED
                            | 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,
                    ),
                    metadata=le_audio.Metadata([
                        le_audio.Metadata.Entry(
                            le_audio.Metadata.Tag.VENDOR_SPECIFIC,
                            data=struct.pack(
                                "<HBBB",
                                _VENDOR_COMPANY_ID_GOOGLE,
                                _HEADTACKER_METADATA_LENGTH,
                                _HEADTACKER_METADATA_TYPE_VALUE,
                                _HeadtrackerTransport.ACL.value,
                            ),
                        )
                    ]),
                ),
            ],
        )
    )
    self.ref_ascs = ascs.AudioStreamControlService(
        self.ref.device,
        sink_ase_id=[1],
        source_ase_id=[2],
    )
    self.ref.device.add_service(self.ref_ascs)

  def _setup_hid_service(self) -> None:
    self.ref_headtracker_report_characteristic = gatt.Characteristic(
        _REPORT_CHACTERISTIC_UUID,
        gatt.Characteristic.Properties.READ
        | gatt.Characteristic.Properties.WRITE
        | gatt.Characteristic.Properties.NOTIFY,
        gatt.Characteristic.READABLE | gatt.Characteristic.WRITEABLE,
    )
    self.ref_headtracker_service = gatt.Service(
        _SERVICE_UUID,
        [
            gatt.Characteristic(
                _VERSION_CHARACTERISTIC_UUID,
                gatt.Characteristic.Properties.READ,
                gatt.Characteristic.READABLE,
                b"#AndroidHeadTracker#2.0#1"
                + bytes(8)
                + b"BT"
                + bytes(self.ref.device.random_address)[::-1],
            ),
            gatt_adapters.SerializableCharacteristicAdapter(
                gatt.Characteristic(
                    _CONTROL_CHARACTERISTIC_UUID,
                    gatt.Characteristic.Properties.READ,
                    gatt.Characteristic.READABLE,
                    value=_HeadtrackerReport(
                        reporting_state=True,
                        power_state=True,
                        report_interval_ms=10,
                        transport=_HeadtrackerReport.Transport.ACL,
                    ),
                ),
                _HeadtrackerReport,
            ),
            self.ref_headtracker_report_characteristic,
        ],
    )
    self.ref.device.add_service(self.ref_headtracker_service)

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.device.adb.getprop(hid.PROPERTY_HID_HOST_SUPPORTED) != "true":
      raise signals.TestAbortClass("HID host is not supported on DUT")
    if not self.dut.is_le_audio_supported:
      raise signals.TestAbortClass("[DUT] Unicast client is not enabled")
    if self.dut.getprop("ro.audio.spatializer_enabled") != "true":
      raise signals.TestAbortClass("Spatializer is not enabled")

    if (
        self.dut.bt.getSdkVersion() >= 35
        and android_constants.AudioDeviceType.BLE_HEADSET
        not in self.dut.bt.getSupportedAudioDeviceTypes(
            android_constants.AudioDeviceRole.OUTPUT
        )
    ):
      raise signals.TestAbortClass("Device does not support LE Audio.")

    self.ref.config.cis_enabled = True
    self.ref.device.cis_enabled = True

    self.setprop_for_class_context(
        _AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST, "true"
    )

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

  async def test_enable_headtracker(self) -> None:
    """Tests enabling headtracker.

    Test steps:
      1. Establish the HID connection between DUT and REF.
      2. Verify the HID connection is established.
      3. Verify the LE Audio connection is established.
      4. Add compatible spatizlier device.
      5. Verify the compatible spatizlier device is added.
      6. Enable headtracker.
      7. Verify the headtracker is enabled.
    """
    self.dut.bt.setSpatializerEnabled(True)
    self._setup_hid_service()
    self._setup_lea_services()
    dut_hid_cb = self.dut.bl4a.register_callback(bl4a_api.Module.HID_HOST)
    dut_lea_cb = self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO)
    self.test_case_context.enter_context(dut_hid_cb)
    self.test_case_context.enter_context(dut_lea_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,
        ),
    )
    self.logger.info("[DUT] Wait for LE Audio active device changed")
    await dut_lea_cb.wait_for_event(
        bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
    )

    self.logger.info("[DUT] Add compatible spatizlier device")
    self.dut.bt.addCompatibleSpatizlierDevice(
        android_constants.AudioDeviceRole.OUTPUT,
        android_constants.AudioDeviceType.BLE_HEADSET,
        self.ref.random_address,
    )

    compatible_spatizlier_devices = self.dut.bt.getCompatibleSpatizlierDevices()
    self.logger.info(
        "[DUT] Compatible Spatizlier devices: %s",
        compatible_spatizlier_devices,
    )
    self.assertIn(self.ref.random_address, compatible_spatizlier_devices)

    self.logger.info("[DUT] Set headtracker enabled")
    self.dut.bt.setHeadtrackerEnabled(
        android_constants.AudioDeviceRole.OUTPUT,
        android_constants.AudioDeviceType.BLE_HEADSET,
        self.ref.random_address,
        True,
    )

    is_headtracker_enabled = self.dut.bt.getHeadtrackerEnabled(
        android_constants.AudioDeviceRole.OUTPUT,
        android_constants.AudioDeviceType.BLE_HEADSET,
        self.ref.random_address,
    )
    self.logger.info("[DUT] Is headtracker enabled: %s", is_headtracker_enabled)
    self.assertTrue(is_headtracker_enabled)

Tests enabling headtracker.

Test steps
  1. Establish the HID connection between DUT and REF.
  2. Verify the HID connection is established.
  3. Verify the LE Audio connection is established.
  4. Add compatible spatizlier device.
  5. Verify the compatible spatizlier device is added.
  6. Enable headtracker.
  7. Verify the headtracker is enabled.
Source code in navi/tests/functionality/hid_headtracker_test.py
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
async def test_enable_headtracker(self) -> None:
  """Tests enabling headtracker.

  Test steps:
    1. Establish the HID connection between DUT and REF.
    2. Verify the HID connection is established.
    3. Verify the LE Audio connection is established.
    4. Add compatible spatizlier device.
    5. Verify the compatible spatizlier device is added.
    6. Enable headtracker.
    7. Verify the headtracker is enabled.
  """
  self.dut.bt.setSpatializerEnabled(True)
  self._setup_hid_service()
  self._setup_lea_services()
  dut_hid_cb = self.dut.bl4a.register_callback(bl4a_api.Module.HID_HOST)
  dut_lea_cb = self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO)
  self.test_case_context.enter_context(dut_hid_cb)
  self.test_case_context.enter_context(dut_lea_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,
      ),
  )
  self.logger.info("[DUT] Wait for LE Audio active device changed")
  await dut_lea_cb.wait_for_event(
      bl4a_api.ProfileActiveDeviceChanged(address=self.ref.random_address),
  )

  self.logger.info("[DUT] Add compatible spatizlier device")
  self.dut.bt.addCompatibleSpatizlierDevice(
      android_constants.AudioDeviceRole.OUTPUT,
      android_constants.AudioDeviceType.BLE_HEADSET,
      self.ref.random_address,
  )

  compatible_spatizlier_devices = self.dut.bt.getCompatibleSpatizlierDevices()
  self.logger.info(
      "[DUT] Compatible Spatizlier devices: %s",
      compatible_spatizlier_devices,
  )
  self.assertIn(self.ref.random_address, compatible_spatizlier_devices)

  self.logger.info("[DUT] Set headtracker enabled")
  self.dut.bt.setHeadtrackerEnabled(
      android_constants.AudioDeviceRole.OUTPUT,
      android_constants.AudioDeviceType.BLE_HEADSET,
      self.ref.random_address,
      True,
  )

  is_headtracker_enabled = self.dut.bt.getHeadtrackerEnabled(
      android_constants.AudioDeviceRole.OUTPUT,
      android_constants.AudioDeviceType.BLE_HEADSET,
      self.ref.random_address,
  )
  self.logger.info("[DUT] Is headtracker enabled: %s", is_headtracker_enabled)
  self.assertTrue(is_headtracker_enabled)

Bases: MultiDevicesTestBase

Tests for LE Audio Unicast client functionality, where the remote device set contains two individual devices.

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/functionality/le_audio_unicast_client_dual_device_test.py
 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
class LeAudioUnicastClientDualDeviceTest(navi_test_base.MultiDevicesTestBase):
  """Tests for LE Audio Unicast client functionality, where the remote device set contains two individual devices.

  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)
  """

  first_bond_timestamp: datetime.datetime | None = None
  dut_vcp_enabled: bool = False
  dut_mcp_enabled: bool = False
  dut_ccp_enabled: bool = False

  def _setup_unicast_server(
      self,
      ref: device.Device,
      audio_location: bap.AudioLocation,
      sirk: bytes,
      sirk_type: csip.SirkType,
  ) -> None:
    ref.add_services([
        pacs.make_pacs(audio_location),
        ascs.AudioStreamControlService(
            ref,
            sink_ase_id=[1],
            source_ase_id=[2],
        ),
        cap.CommonAudioServiceService(
            csip.CoordinatedSetIdentificationService(
                set_identity_resolving_key=sirk,
                set_identity_resolving_key_type=sirk_type,
                coordinated_set_size=2,
            )
        ),
        vcs.VolumeControlService(),
    ])

  @retry.retry_on_exception()
  async def _pair_major_device(self) -> None:
    ref_address = self.refs[0].random_address
    with (
        self.dut.bl4a.register_callback(
            bl4a_api.Module.ADAPTER
        ) as dut_adapter_cb,
        self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_lea_cb,
    ):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[REF|%s] Start advertising", ref_address)
        csis = _get_service_from_device(
            self.refs[0].device, csip.CoordinatedSetIdentificationService
        )
        await self.refs[0].device.create_advertising_set(
            advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
            advertising_data=bytes(
                bap.UnicastServerAdvertisingData(
                    announcement_type=bap.AnnouncementType.GENERAL
                )
            )
            + csis.get_advertising_data(),
        )

      self.logger.info("[DUT] Create bond with %s", ref_address)
      self.dut.bt.createBond(
          ref_address,
          android_constants.Transport.LE,
          android_constants.AddressTypeStatus.RANDOM,
      )
      self.logger.info("[DUT] Wait for pairing request")
      await dut_adapter_cb.wait_for_event(
          bl4a_api.PairingRequest,
          lambda e: e.address == ref_address,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      self.logger.info("[DUT] Accept pairing request")
      self.assertTrue(self.dut.bt.setPairingConfirmation(ref_address, True))
      self.logger.info("[DUT] Wait for bond state change")
      event = await dut_adapter_cb.wait_for_event(
          bl4a_api.BondStateChanged,
          lambda e: e.address == ref_address
          and e.state in _TERMINATION_BOND_STATES,
      )
      self.assertEqual(event.state, android_constants.BondState.BONDED)
      self.first_bond_timestamp = datetime.datetime.now()

      self.logger.info("[DUT] Wait for UUID Change")
      await dut_adapter_cb.wait_for_event(
          bl4a_api.UuidChanged(address=ref_address, uuids=mock.ANY)
      )
      self.dut.bt.connect(ref_address)

      self.logger.info("[DUT] Wait for LE Audio connected")
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=ref_address, state=_ConnectionState.CONNECTED
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  @retry.retry_on_exception()
  async def _pair_minor_device(self) -> None:
    if not self.first_bond_timestamp:
      self.fail("Major device has not been paired")

    ref_address = self.refs[1].random_address
    with (
        self.dut.bl4a.register_callback(
            bl4a_api.Module.ADAPTER
        ) as dut_adapter_cb,
        self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_lea_cb,
    ):
      csis = _get_service_from_device(
          self.refs[1].device, csip.CoordinatedSetIdentificationService
      )
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        self.logger.info("[REF|%s] Start advertising", ref_address)
        await self.refs[1].device.create_advertising_set(
            advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
            advertising_data=bytes(
                core.AdvertisingData([
                    (
                        core.AdvertisingData.COMPLETE_LOCAL_NAME,
                        bytes("Bumble Right", "utf-8"),
                    ),
                    (
                        core.AdvertisingData.FLAGS,
                        bytes([
                            core.AdvertisingData.LE_GENERAL_DISCOVERABLE_MODE_FLAG
                        ]),
                    ),
                    (
                        core.AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
                        bytes(csip.CoordinatedSetIdentificationService.UUID),
                    ),
                ])
            )
            + bytes(
                bap.UnicastServerAdvertisingData(
                    announcement_type=bap.AnnouncementType.GENERAL,
                    available_audio_contexts=bap.ContextType(0xFFFF),
                )
            )
            + csis.get_advertising_data(),
        )

      # When CSIS set member is discovered, Settings should automatically pair
      # to it and accept the pairing request.
      # However, Settings may not start discovery automatically, so here we need
      # to discover the CSIS set member manually.
      self.logger.info("[DUT] Start discovery")
      self.dut.bt.startInquiry()
      self.logger.info("[DUT] Wait for 2nd pairing request")
      await dut_adapter_cb.wait_for_event(
          bl4a_api.PairingRequest,
          lambda e: e.address == ref_address,
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      if (
          elapsed_time := (datetime.datetime.now() - self.first_bond_timestamp)
      ) > datetime.timedelta(seconds=10):
        self.logger.info(
            "Pairing request takes %.2fs > 10s, need to manually accept",
            elapsed_time.total_seconds(),
        )
        self.dut.bt.setPairingConfirmation(ref_address, True)

      self.logger.info("[DUT] Wait for 2nd REF to be bonded")
      event = await dut_adapter_cb.wait_for_event(
          bl4a_api.BondStateChanged,
          lambda e: e.address == ref_address
          and e.state in _TERMINATION_BOND_STATES,
      )
      self.assertEqual(event.state, android_constants.BondState.BONDED)

      self.logger.info("[DUT] Wait for UUID Change")
      await dut_adapter_cb.wait_for_event(
          bl4a_api.UuidChanged(address=ref_address, uuids=mock.ANY)
      )
      self.dut.bt.connect(ref_address)

      self.logger.info("[DUT] Wait for 2nd REF to be connected")
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=ref_address, state=_ConnectionState.CONNECTED
          ),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )

  _PROXY = TypeVar("_PROXY", bound=gatt_client.ProfileServiceProxy)

  async def _make_service_client(
      self, ref: device.Device, proxy_class: type[_PROXY]
  ) -> _PROXY:
    self.logger.info("[REF] Connect %s", proxy_class.__name__)
    ref_dut_acl = ref.find_connection_by_bd_addr(
        hci.Address(self.dut.address), transport=core.BT_LE_TRANSPORT
    )
    if not ref_dut_acl:
      self.fail("No ACL connection found")
    async with device.Peer(ref_dut_acl) as peer:
      client = peer.create_service_proxy(proxy_class)
      if not client:
        self.fail("Failed to connect %s", proxy_class.__name__)
      return client

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if not self.dut.is_le_audio_supported:
      raise signals.TestAbortClass("[DUT] Device does not support LE Audio.")

    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"
    )
    for ref in self.refs:
      ref.config.cis_enabled = True
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref.open()
        if not ref.device.supports_le_features(
            hci.LeFeatureMask.CONNECTED_ISOCHRONOUS_STREAM_PERIPHERAL
        ):
          raise signals.TestAbortClass("REF does not support CIS peripheral")

    self.setprop_for_class_context(
        _AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST, "true"
    )

    # Always repeat audio to avoid audio stopping.
    self.dut.bt.audioSetRepeat(android_constants.RepeatMode.ONE)

  @override
  async def async_setup_test(self) -> None:
    await super().async_setup_test()
    sirk = secrets.token_bytes(csip.SET_IDENTITY_RESOLVING_KEY_LENGTH)
    for ref, audio_location in zip(
        self.refs, (bap.AudioLocation.FRONT_LEFT, bap.AudioLocation.FRONT_RIGHT)
    ):
      self._setup_unicast_server(
          ref=ref.device,
          audio_location=audio_location,
          sirk=sirk,
          sirk_type=csip.SirkType.ENCRYPTED,
      )
      # Override pairing config factory to set identity address type and
      # io capability.
      ref.device.pairing_config_factory = lambda _: pairing.PairingConfig(
          identity_address_type=pairing.PairingConfig.AddressType.RANDOM,
          delegate=pairing.PairingDelegate(),
      )
    self.first_bond_timestamp = None

  @override
  async def async_teardown_test(self) -> None:
    await super().async_teardown_test()
    # Make sure audio is stopped before starting the test.
    await asyncio.to_thread(self.dut.bt.audioStop)

  @navi_test_base.named_parameterized(
      plaintext_sirk=dict(sirk_type=csip.SirkType.PLAINTEXT),
      encrypted_sirk=dict(sirk_type=csip.SirkType.ENCRYPTED),
  )
  async def test_pair_and_connect(self, sirk_type: csip.SirkType) -> None:
    """Tests pairing and connecting to Unicast servers in a CSIP set.

    Test steps:
      1. Override the SIRK type for all refs.
      2. Pair and connect the major device.
      3. Pair and connect the minor device.
      4. Check if both devices are connected and active.

    Args:
      sirk_type: The SIRK type to use.
    """
    # Override the SIRK type for all refs.
    for ref in self.refs:
      csis = _get_service_from_device(
          ref.device, csip.CoordinatedSetIdentificationService
      )
      csis.set_identity_resolving_key_type = sirk_type

    # Pair and connect devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    # Check if both devices are connected and active.
    self.assertCountEqual(
        self.dut.bt.getActiveDevices(android_constants.Profile.LE_AUDIO),
        [self.refs[0].random_address, self.refs[1].random_address],
    )

  @navi_test_base.named_parameterized(
      ("active", True),
      ("passive", False),
  )
  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 self.dut.device.is_emulator and not is_active:
      self.skipTest("Rootcanal doesn't support APCF.")

    # Pair and connect devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      for ref in self.refs:
        if is_active:
          await self.disconnect_with_check(
              ref.random_address, android_constants.Transport.LE, ref
          )
        else:
          if not (
              ref_dut_acl := ref.device.find_connection_by_bd_addr(
                  hci.Address(self.dut.address), transport=core.BT_LE_TRANSPORT
              )
          ):
            self.fail("Unable to find connection between REF and DUT")
          async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
            self.logger.info("[REF] Disconnect DUT")
            await ref_dut_acl.disconnect()

        self.logger.info("[DUT] Wait for disconnected")
        await dut_cb.wait_for_event(
            bl4a_api.AclDisconnected(
                address=ref.random_address,
                transport=android_constants.Transport.LE,
            )
        )

    with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
      for ref in self.refs:
        self.logger.info("[REF] Start advertising")
        announcement_type = (
            bap.AnnouncementType.GENERAL
            if is_active
            else bap.AnnouncementType.TARGETED
        )
        bap_announcement = ascs.make_bap_announcement(
            announcement_type=(announcement_type),
            available_audio_contexts=bap.ContextType(0xFFFF),
        )
        cap_announcement = ascs.make_cap_announcement(announcement_type)
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          await ref.device.create_advertising_set(
              advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
              advertising_data=bytes(
                  core.AdvertisingData([
                      _GENERAL_DISCOVERABLE_AD_FLAGS,
                      bap_announcement,
                      cap_announcement,
                  ])
              ),
          )
        if is_active:
          self.logger.info("[DUT] Reconnect REF")
          self.dut.bt.connect(ref.random_address)

        self.logger.info("[DUT] Wait for LE Audio connected")
        await dut_cb.wait_for_event(
            bl4a_api.ProfileConnectionStateChanged(
                address=ref.random_address, state=_ConnectionState.CONNECTED
            ),
        )

    # Check if both devices are connected and active.
    self.assertCountEqual(
        self.dut.bt.getActiveDevices(android_constants.Profile.LE_AUDIO),
        [self.refs[0].random_address, self.refs[1].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.
    """
    # Pair and connect devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    sink_ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
    for ref in self.refs:
      sink_ase_states.extend(
          pyee_extensions.EventTriggeredValueObserver(
              ase,
              ase.EVENT_STATE_CHANGE,
              functools.partial(
                  lambda ase: cast(
                      ascs.AudioStreamEndpointCharacteristic, ase
                  ).state,
                  ase,
              ),
          )
          for ase in _get_service_from_device(
              ref.device, ascs.AudioStreamControlService
          ).ase_state_machines.values()
          if ase.role == ascs.AudioRole.SINK
      )

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.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):
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )

    # 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)
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.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.
    """
    # Pair and connect devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
    for ref in self.refs:
      ase_states.extend(
          pyee_extensions.EventTriggeredValueObserver(
              ase,
              ase.EVENT_STATE_CHANGE,
              functools.partial(
                  lambda ase: cast(
                      ascs.AudioStreamEndpointCharacteristic, ase
                  ).state,
                  ase,
              ),
          )
          for ase in _get_service_from_device(
              ref.device, ascs.AudioStreamControlService
          ).ase_state_machines.values()
      )

    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,
    )

    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):
        for ase_state in ase_states:
          await ase_state.wait_for_target_value(
              ascs.AudioStreamEndpointCharacteristic.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):
        # With current configuration, all ASEs will be active in bidirectional
        # streaming.
        for ase_state in ase_states:
          await ase_state.wait_for_target_value(
              ascs.AudioStreamEndpointCharacteristic.State.STREAMING
          )

      # 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)

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for ase_state in ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.IDLE
        )

  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.
    """
    # Pair and connect devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    sink_ases: list[ascs.AudioStreamEndpointCharacteristic] = []
    for ref in self.refs:
      sink_ases.extend(
          ase
          for ase in _get_service_from_device(
              ref.device, ascs.AudioStreamControlService
          ).ase_state_machines.values()
          if ase.role == ascs.AudioRole.SINK
      )
    sink_ase_states = [
        pyee_extensions.EventTriggeredValueObserver(
            ase,
            ase.EVENT_STATE_CHANGE,
            functools.partial(
                lambda ase: cast(
                    ascs.AudioStreamEndpointCharacteristic, ase
                ).state,
                ase,
            ),
        )
        for ase in sink_ases
    ]

    # Make sure audio is not streaming.
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.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):
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )
    for sink_ase in sink_ases:
      self.assertIsInstance(sink_ase.metadata, le_audio.Metadata)
      if (entry := _get_audio_context_entry(sink_ase)) is None:
        self.fail("Audio context is not found")
      context_type = struct.unpack_from("<H", entry.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):
        self.logger.info("[DUT] Wait for ASE to be released")
        for ase_state in sink_ase_states:
          await ase_state.wait_for_target_value(
              ascs.AudioStreamEndpointCharacteristic.State.IDLE
          )
        self.logger.info("[DUT] Wait for ASE to be reconfigured")
        for ase_state in sink_ase_states:
          await ase_state.wait_for_target_value(
              ascs.AudioStreamEndpointCharacteristic.State.STREAMING
          )
        for sink_ase in sink_ases:
          if (entry := _get_audio_context_entry(sink_ase)) is None:
            self.fail("Audio context is not found")
          context_type = struct.unpack_from("<H", entry.data)[0]
          self.assertTrue(context_type & bap.ContextType.CONVERSATIONAL)

  async def test_streaming_later_join(self) -> None:
    """Tests connecting to devices later during streaming.

    Test steps:
      1. Start audio streaming from DUT.
      2. Start advertising from REF(Left), wait for DUT to connect.
      3. Wait for audio streaming to start from REF(Left).
      4. Start advertising from REF(Right), wait for DUT to connect.
      5. Wait for audio streaming to start from REF(Right).
    """
    if self.dut.device.is_emulator:
      self.skipTest("Rootcanal doesn't support APCF.")

    # Pair and connect the major device.
    await self._pair_major_device()
    await self._pair_minor_device()

    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      for ref in self.refs:
        self.logger.info("[DUT] Disconnect REF")
        self.dut.bt.disconnect(ref.random_address)
        self.logger.info("[DUT] Wait for disconnected")
        await dut_cb.wait_for_event(
            bl4a_api.AclDisconnected(
                address=ref.random_address,
                transport=android_constants.Transport.LE,
            )
        )

    sink_ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
    for ref in self.refs:
      sink_ase_states.extend(
          pyee_extensions.EventTriggeredValueObserver(
              ase,
              ase.EVENT_STATE_CHANGE,
              functools.partial(
                  lambda ase: cast(
                      ascs.AudioStreamEndpointCharacteristic, ase
                  ).state,
                  ase,
              ),
          )
          for ase in _get_service_from_device(
              ref.device, ascs.AudioStreamControlService
          ).ase_state_machines.values()
          if ase.role == ascs.AudioRole.SINK
      )

    self.logger.info("[DUT] Start audio streaming")
    await asyncio.to_thread(self.dut.bt.audioPlaySine)

    for sink_ase, ref in zip(sink_ase_states, self.refs):
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref.device.create_advertising_set(
            advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
            advertising_data=bytes(
                core.AdvertisingData([
                    ascs.make_bap_announcement(
                        announcement_type=bap.AnnouncementType.TARGETED
                    ),
                    _GENERAL_DISCOVERABLE_AD_FLAGS,
                ])
            ),
        )
        self.logger.info("[REF] Wait for ASE to be streaming")
        await sink_ase.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )

  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")

    # Pair and connect the devices.
    await self._pair_major_device()
    await self._pair_minor_device()

    # Wait for the volume to be stable.
    await asyncio.sleep(_PREPARE_TIME_SECONDS)

    ratio = vcs.MAX_VOLUME / self.dut.bt.getMaxVolume(_StreamType.MUSIC)
    dut_volume = self.dut.bt.getVolume(_StreamType.MUSIC)
    ref_expected_volume = decimal.Decimal(dut_volume * ratio).to_integral_exact(
        rounding=decimal.ROUND_HALF_UP
    )
    # The behavior to set volume is not clear, but we can make sure the volume
    # should be correctly synchronized between DUT and all REF devices.
    for ref in self.refs:
      ref_vcs = _get_service_from_device(ref.device, vcs.VolumeControlService)
      self.assertEqual(ref_expected_volume, ref_vcs.volume_setting)

  @navi_test_base.named_parameterized(
      dict(
          testcase_name="from_dut",
          issuer=constants.TestRole.DUT,
      ),
      dict(
          testcase_name="from_ref",
          issuer=constants.TestRole.REF,
      ),
  )
  async def test_set_volume(self, issuer: constants.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 devices.

    Args:
      issuer: The role that issues the volume setting request.
    """
    if not self.dut_vcp_enabled:
      self.skipTest("VCP is not enabled on DUT")

    await self._pair_major_device()
    await self._pair_minor_device()

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

    def dut_to_ref_volume(dut_volume: int) -> int:
      return int(
          decimal.Decimal(
              dut_volume / dut_max_volume * vcs.MAX_VOLUME
          ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
      )

    def get_volume_setting(service: vcs.VolumeControlService) -> int:
      return service.volume_setting

    # 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)

    ref_vcs_services = [
        _get_service_from_device(ref.device, vcs.VolumeControlService)
        for ref in self.refs
    ]

    for dut_volume in range(dut_max_volume + 1):
      ref_volume = dut_to_ref_volume(dut_volume)
      if self.dut.bt.getVolume(_StreamType.MUSIC) == dut_volume:
        # Skip if DUT volume is already set to the target.
        continue
      with self.dut.bl4a.register_callback(
          bl4a_api.Module.AUDIO
      ) as dut_audio_cb:
        if issuer == constants.TestRole.DUT:
          self.logger.info("[DUT] Set volume to %d", dut_volume)
          self.dut.bt.setVolume(_StreamType.MUSIC, dut_volume)
        else:
          self.logger.info("[REF] Set volume to %d", dut_volume)
          ref_vcs_services[0].volume_setting = ref_volume
          await self.refs[0].device.notify_subscribers(
              ref_vcs_services[0].volume_state
          )

        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          for i, ref_vcs_service in enumerate(ref_vcs_services):
            self.logger.info(
                "[REF-%d] Wait for volume to be set to %d", i, ref_volume
            )
            await pyee_extensions.EventTriggeredValueObserver[int](
                ref_vcs_service,
                ref_vcs_service.EVENT_VOLUME_STATE_CHANGE,
                functools.partial(get_volume_setting, ref_vcs_service),
            ).wait_for_target_value(ref_volume)
        # Only when remote device sets volume, DUT can receive the intent.
        if issuer == constants.TestRole.REF:
          self.logger.info("[DUT] Wait for volume to be set")
          await dut_audio_cb.wait_for_event(
              bl4a_api.VolumeChanged(
                  stream_type=_StreamType.MUSIC, volume_value=dut_volume
              ),
          )

  async def test_mcp_play_pause(self) -> None:
    """Tests playing and pausing media playback over MCP.

    Test steps:
      1. Connect MCP.
      2. Subscribe MCP characteristics.
      3. Play media playback on DUT.
      4. Pause media playback over MCP.
      5. Wait for playback to pause.
      6. Resume media playback over MCP.
      7. Wait for playback to start.
    """
    if not self.dut_mcp_enabled:
      self.skipTest("MCP is not enabled on DUT")

    await self._pair_major_device()
    await self._pair_minor_device()

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Connect GMCS")
      ref_mcp_clients = await asyncio.gather(*[
          self._make_service_client(
              ref.device, _GenericMediaControlServiceProxy
          )
          for ref in self.refs
      ])
      self.logger.info("[REF] Subscribe MCP characteristics")
      await asyncio.gather(*[
          ref_mcp_client.subscribe_characteristics()
          for ref_mcp_client in ref_mcp_clients
      ])

    media_states = [
        await gatt_helper.MutableCharacteristicState.create(
            ref_mcp_client.media_state
        )
        for ref_mcp_client in ref_mcp_clients
        if ref_mcp_client.media_state
    ]
    self.assertLen(media_states, self.NUM_REF_DEVICES)

    dut_player_cb = self.dut.bl4a.register_callback(bl4a_api.Module.PLAYER)
    self.test_case_context.push(dut_player_cb)

    self.logger.info("[DUT] Play")
    await asyncio.to_thread(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)
    )

    for i, media_state in enumerate(media_states):
      self.logger.info(
          "[REF-%d] Wait for media state to be PLAYING", i
      )
      await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Pause"
    ):
      # Pause from the first REF device.
      result = await ref_mcp_clients[0].write_control_point(_McpOpcode.PAUSE)
      self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)
      for i, media_state in enumerate(media_states):
        self.logger.info("[REF-%d] Wait for media state to be PAUSED", i)
        await media_state.wait_for_target_value(bytes([mcp.MediaState.PAUSED]))
    self.logger.info("[DUT] Wait for playback paused")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=False),
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Play"
    ):
      # Resume from the second REF device.
      result = await ref_mcp_clients[1].write_control_point(_McpOpcode.PLAY)
      self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)
      for i, media_state in enumerate(media_states):
        self.logger.info("[REF-%d] Wait for media state to be PLAYING", i)
        await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))
    self.logger.info("[DUT] Wait for playback started")
    await dut_player_cb.wait_for_event(
        bl4a_api.PlayerIsPlayingChanged(is_playing=True)
    )

  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")

    await self._pair_major_device()
    await self._pair_minor_device()

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      self.logger.info("[REF] Connect TBS")
      ref_tbs_clients = await asyncio.gather(*[
          self._make_service_client(
              ref.device, ccp.GenericTelephoneBearerServiceProxy
          )
          for ref in self.refs
      ])
      self.logger.info("[REF] Read and subscribe TBS characteristics")
      await asyncio.gather(*[
          ref_tbs_client.read_and_subscribe_characteristics()
          for ref_tbs_client in ref_tbs_clients
      ])

    expected_call_index = 1
    call = self.dut.bl4a.make_phone_call(
        _CALLER_NAME, _CALLER_NUMBER, _Direction.INCOMING
    )
    self.test_case_context.push(call)
    dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
    self.test_case_context.push(dut_telecom_cb)
    for i, ref_tbs_client in enumerate(ref_tbs_clients):
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg=f"[REF-{i}] 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-0] Accept call"
    ):
      await ref_tbs_clients[0].accept(expected_call_index)

    self.logger.info("[DUT] Wait for call to be active")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            state=_CallState.ACTIVE, handle=mock.ANY, name=mock.ANY
        ),
    )

    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF-1] Terminate call"
    ):
      await ref_tbs_clients[1].terminate(expected_call_index)

    self.logger.info("[DUT] Wait for call to be disconnected")
    await dut_telecom_cb.wait_for_event(
        bl4a_api.CallStateChanged(
            state=_CallState.DISCONNECTED, handle=mock.ANY, name=mock.ANY
        ),
    )
    for i, ref_tbs_client in enumerate(ref_tbs_clients):
      async with self.assert_not_timeout(
          _DEFAULT_STEP_TIMEOUT_SECONDS,
          msg=f"[REF-{i}] Wait for call state change",
      ):
        await ref_tbs_client.call_state.wait_for_target_value(b"")

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/functionality/le_audio_unicast_client_dual_device_test.py
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
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.
  """
  # Pair and connect devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
  for ref in self.refs:
    ase_states.extend(
        pyee_extensions.EventTriggeredValueObserver(
            ase,
            ase.EVENT_STATE_CHANGE,
            functools.partial(
                lambda ase: cast(
                    ascs.AudioStreamEndpointCharacteristic, ase
                ).state,
                ase,
            ),
        )
        for ase in _get_service_from_device(
            ref.device, ascs.AudioStreamControlService
        ).ase_state_machines.values()
    )

  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,
  )

  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):
      for ase_state in ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.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):
      # With current configuration, all ASEs will be active in bidirectional
      # streaming.
      for ase_state in ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )

    # 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)

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    for ase_state in ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.State.IDLE
      )

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/functionality/le_audio_unicast_client_dual_device_test.py
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
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")

  await self._pair_major_device()
  await self._pair_minor_device()

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Connect TBS")
    ref_tbs_clients = await asyncio.gather(*[
        self._make_service_client(
            ref.device, ccp.GenericTelephoneBearerServiceProxy
        )
        for ref in self.refs
    ])
    self.logger.info("[REF] Read and subscribe TBS characteristics")
    await asyncio.gather(*[
        ref_tbs_client.read_and_subscribe_characteristics()
        for ref_tbs_client in ref_tbs_clients
    ])

  expected_call_index = 1
  call = self.dut.bl4a.make_phone_call(
      _CALLER_NAME, _CALLER_NUMBER, _Direction.INCOMING
  )
  self.test_case_context.push(call)
  dut_telecom_cb = self.dut.bl4a.register_callback(bl4a_api.Module.TELECOM)
  self.test_case_context.push(dut_telecom_cb)
  for i, ref_tbs_client in enumerate(ref_tbs_clients):
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg=f"[REF-{i}] 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-0] Accept call"
  ):
    await ref_tbs_clients[0].accept(expected_call_index)

  self.logger.info("[DUT] Wait for call to be active")
  await dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          state=_CallState.ACTIVE, handle=mock.ANY, name=mock.ANY
      ),
  )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF-1] Terminate call"
  ):
    await ref_tbs_clients[1].terminate(expected_call_index)

  self.logger.info("[DUT] Wait for call to be disconnected")
  await dut_telecom_cb.wait_for_event(
      bl4a_api.CallStateChanged(
          state=_CallState.DISCONNECTED, handle=mock.ANY, name=mock.ANY
      ),
  )
  for i, ref_tbs_client in enumerate(ref_tbs_clients):
    async with self.assert_not_timeout(
        _DEFAULT_STEP_TIMEOUT_SECONDS,
        msg=f"[REF-{i}] Wait for call state change",
    ):
      await ref_tbs_client.call_state.wait_for_target_value(b"")

Tests playing and pausing media playback over MCP.

Test steps
  1. Connect MCP.
  2. Subscribe MCP characteristics.
  3. Play media playback on DUT.
  4. Pause media playback over MCP.
  5. Wait for playback to pause.
  6. Resume media playback over MCP.
  7. Wait for playback to start.
Source code in navi/tests/functionality/le_audio_unicast_client_dual_device_test.py
 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
async def test_mcp_play_pause(self) -> None:
  """Tests playing and pausing media playback over MCP.

  Test steps:
    1. Connect MCP.
    2. Subscribe MCP characteristics.
    3. Play media playback on DUT.
    4. Pause media playback over MCP.
    5. Wait for playback to pause.
    6. Resume media playback over MCP.
    7. Wait for playback to start.
  """
  if not self.dut_mcp_enabled:
    self.skipTest("MCP is not enabled on DUT")

  await self._pair_major_device()
  await self._pair_minor_device()

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    self.logger.info("[REF] Connect GMCS")
    ref_mcp_clients = await asyncio.gather(*[
        self._make_service_client(
            ref.device, _GenericMediaControlServiceProxy
        )
        for ref in self.refs
    ])
    self.logger.info("[REF] Subscribe MCP characteristics")
    await asyncio.gather(*[
        ref_mcp_client.subscribe_characteristics()
        for ref_mcp_client in ref_mcp_clients
    ])

  media_states = [
      await gatt_helper.MutableCharacteristicState.create(
          ref_mcp_client.media_state
      )
      for ref_mcp_client in ref_mcp_clients
      if ref_mcp_client.media_state
  ]
  self.assertLen(media_states, self.NUM_REF_DEVICES)

  dut_player_cb = self.dut.bl4a.register_callback(bl4a_api.Module.PLAYER)
  self.test_case_context.push(dut_player_cb)

  self.logger.info("[DUT] Play")
  await asyncio.to_thread(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)
  )

  for i, media_state in enumerate(media_states):
    self.logger.info(
        "[REF-%d] Wait for media state to be PLAYING", i
    )
    await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Pause"
  ):
    # Pause from the first REF device.
    result = await ref_mcp_clients[0].write_control_point(_McpOpcode.PAUSE)
    self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)
    for i, media_state in enumerate(media_states):
      self.logger.info("[REF-%d] Wait for media state to be PAUSED", i)
      await media_state.wait_for_target_value(bytes([mcp.MediaState.PAUSED]))
  self.logger.info("[DUT] Wait for playback paused")
  await dut_player_cb.wait_for_event(
      bl4a_api.PlayerIsPlayingChanged(is_playing=False),
  )

  async with self.assert_not_timeout(
      _DEFAULT_STEP_TIMEOUT_SECONDS, msg="[REF] Play"
  ):
    # Resume from the second REF device.
    result = await ref_mcp_clients[1].write_control_point(_McpOpcode.PLAY)
    self.assertEqual(result, mcp.MediaControlPointResultCode.SUCCESS)
    for i, media_state in enumerate(media_states):
      self.logger.info("[REF-%d] Wait for media state to be PLAYING", i)
      await media_state.wait_for_target_value(bytes([mcp.MediaState.PLAYING]))
  self.logger.info("[DUT] Wait for playback started")
  await dut_player_cb.wait_for_event(
      bl4a_api.PlayerIsPlayingChanged(is_playing=True)
  )

Tests pairing and connecting to Unicast servers in a CSIP set.

Test steps
  1. Override the SIRK type for all refs.
  2. Pair and connect the major device.
  3. Pair and connect the minor device.
  4. Check if both devices are connected and active.

Parameters:

Name Type Description Default
sirk_type SirkType

The SIRK type to use.

required
Source code in navi/tests/functionality/le_audio_unicast_client_dual_device_test.py
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
@navi_test_base.named_parameterized(
    plaintext_sirk=dict(sirk_type=csip.SirkType.PLAINTEXT),
    encrypted_sirk=dict(sirk_type=csip.SirkType.ENCRYPTED),
)
async def test_pair_and_connect(self, sirk_type: csip.SirkType) -> None:
  """Tests pairing and connecting to Unicast servers in a CSIP set.

  Test steps:
    1. Override the SIRK type for all refs.
    2. Pair and connect the major device.
    3. Pair and connect the minor device.
    4. Check if both devices are connected and active.

  Args:
    sirk_type: The SIRK type to use.
  """
  # Override the SIRK type for all refs.
  for ref in self.refs:
    csis = _get_service_from_device(
        ref.device, csip.CoordinatedSetIdentificationService
    )
    csis.set_identity_resolving_key_type = sirk_type

  # Pair and connect devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  # Check if both devices are connected and active.
  self.assertCountEqual(
      self.dut.bt.getActiveDevices(android_constants.Profile.LE_AUDIO),
      [self.refs[0].random_address, self.refs[1].random_address],
  )

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/functionality/le_audio_unicast_client_dual_device_test.py
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
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.
  """
  # Pair and connect devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  sink_ases: list[ascs.AudioStreamEndpointCharacteristic] = []
  for ref in self.refs:
    sink_ases.extend(
        ase
        for ase in _get_service_from_device(
            ref.device, ascs.AudioStreamControlService
        ).ase_state_machines.values()
        if ase.role == ascs.AudioRole.SINK
    )
  sink_ase_states = [
      pyee_extensions.EventTriggeredValueObserver(
          ase,
          ase.EVENT_STATE_CHANGE,
          functools.partial(
              lambda ase: cast(
                  ascs.AudioStreamEndpointCharacteristic, ase
              ).state,
              ase,
          ),
      )
      for ase in sink_ases
  ]

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    for ase_state in sink_ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.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):
    for ase_state in sink_ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.State.STREAMING
      )
  for sink_ase in sink_ases:
    self.assertIsInstance(sink_ase.metadata, le_audio.Metadata)
    if (entry := _get_audio_context_entry(sink_ase)) is None:
      self.fail("Audio context is not found")
    context_type = struct.unpack_from("<H", entry.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):
      self.logger.info("[DUT] Wait for ASE to be released")
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.IDLE
        )
      self.logger.info("[DUT] Wait for ASE to be reconfigured")
      for ase_state in sink_ase_states:
        await ase_state.wait_for_target_value(
            ascs.AudioStreamEndpointCharacteristic.State.STREAMING
        )
      for sink_ase in sink_ases:
        if (entry := _get_audio_context_entry(sink_ase)) is None:
          self.fail("Audio context is not found")
        context_type = struct.unpack_from("<H", entry.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/functionality/le_audio_unicast_client_dual_device_test.py
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
@navi_test_base.named_parameterized(
    ("active", True),
    ("passive", False),
)
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 self.dut.device.is_emulator and not is_active:
    self.skipTest("Rootcanal doesn't support APCF.")

  # Pair and connect devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    for ref in self.refs:
      if is_active:
        await self.disconnect_with_check(
            ref.random_address, android_constants.Transport.LE, ref
        )
      else:
        if not (
            ref_dut_acl := ref.device.find_connection_by_bd_addr(
                hci.Address(self.dut.address), transport=core.BT_LE_TRANSPORT
            )
        ):
          self.fail("Unable to find connection between REF and DUT")
        async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
          self.logger.info("[REF] Disconnect DUT")
          await ref_dut_acl.disconnect()

      self.logger.info("[DUT] Wait for disconnected")
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=ref.random_address,
              transport=android_constants.Transport.LE,
          )
      )

  with self.dut.bl4a.register_callback(bl4a_api.Module.LE_AUDIO) as dut_cb:
    for ref in self.refs:
      self.logger.info("[REF] Start advertising")
      announcement_type = (
          bap.AnnouncementType.GENERAL
          if is_active
          else bap.AnnouncementType.TARGETED
      )
      bap_announcement = ascs.make_bap_announcement(
          announcement_type=(announcement_type),
          available_audio_contexts=bap.ContextType(0xFFFF),
      )
      cap_announcement = ascs.make_cap_announcement(announcement_type)
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        await ref.device.create_advertising_set(
            advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
            advertising_data=bytes(
                core.AdvertisingData([
                    _GENERAL_DISCOVERABLE_AD_FLAGS,
                    bap_announcement,
                    cap_announcement,
                ])
            ),
        )
      if is_active:
        self.logger.info("[DUT] Reconnect REF")
        self.dut.bt.connect(ref.random_address)

      self.logger.info("[DUT] Wait for LE Audio connected")
      await dut_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=ref.random_address, state=_ConnectionState.CONNECTED
          ),
      )

  # Check if both devices are connected and active.
  self.assertCountEqual(
      self.dut.bt.getActiveDevices(android_constants.Profile.LE_AUDIO),
      [self.refs[0].random_address, self.refs[1].random_address],
  )

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 devices.

Parameters:

Name Type Description Default
issuer TestRole

The role that issues the volume setting request.

required
Source code in navi/tests/functionality/le_audio_unicast_client_dual_device_test.py
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
@navi_test_base.named_parameterized(
    dict(
        testcase_name="from_dut",
        issuer=constants.TestRole.DUT,
    ),
    dict(
        testcase_name="from_ref",
        issuer=constants.TestRole.REF,
    ),
)
async def test_set_volume(self, issuer: constants.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 devices.

  Args:
    issuer: The role that issues the volume setting request.
  """
  if not self.dut_vcp_enabled:
    self.skipTest("VCP is not enabled on DUT")

  await self._pair_major_device()
  await self._pair_minor_device()

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

  def dut_to_ref_volume(dut_volume: int) -> int:
    return int(
        decimal.Decimal(
            dut_volume / dut_max_volume * vcs.MAX_VOLUME
        ).to_integral_exact(rounding=decimal.ROUND_HALF_UP)
    )

  def get_volume_setting(service: vcs.VolumeControlService) -> int:
    return service.volume_setting

  # 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)

  ref_vcs_services = [
      _get_service_from_device(ref.device, vcs.VolumeControlService)
      for ref in self.refs
  ]

  for dut_volume in range(dut_max_volume + 1):
    ref_volume = dut_to_ref_volume(dut_volume)
    if self.dut.bt.getVolume(_StreamType.MUSIC) == dut_volume:
      # Skip if DUT volume is already set to the target.
      continue
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.AUDIO
    ) as dut_audio_cb:
      if issuer == constants.TestRole.DUT:
        self.logger.info("[DUT] Set volume to %d", dut_volume)
        self.dut.bt.setVolume(_StreamType.MUSIC, dut_volume)
      else:
        self.logger.info("[REF] Set volume to %d", dut_volume)
        ref_vcs_services[0].volume_setting = ref_volume
        await self.refs[0].device.notify_subscribers(
            ref_vcs_services[0].volume_state
        )

      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        for i, ref_vcs_service in enumerate(ref_vcs_services):
          self.logger.info(
              "[REF-%d] Wait for volume to be set to %d", i, ref_volume
          )
          await pyee_extensions.EventTriggeredValueObserver[int](
              ref_vcs_service,
              ref_vcs_service.EVENT_VOLUME_STATE_CHANGE,
              functools.partial(get_volume_setting, ref_vcs_service),
          ).wait_for_target_value(ref_volume)
      # Only when remote device sets volume, DUT can receive the intent.
      if issuer == constants.TestRole.REF:
        self.logger.info("[DUT] Wait for volume to be set")
        await dut_audio_cb.wait_for_event(
            bl4a_api.VolumeChanged(
                stream_type=_StreamType.MUSIC, volume_value=dut_volume
            ),
        )

Tests connecting to devices later during streaming.

Test steps
  1. Start audio streaming from DUT.
  2. Start advertising from REF(Left), wait for DUT to connect.
  3. Wait for audio streaming to start from REF(Left).
  4. Start advertising from REF(Right), wait for DUT to connect.
  5. Wait for audio streaming to start from REF(Right).
Source code in navi/tests/functionality/le_audio_unicast_client_dual_device_test.py
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
async def test_streaming_later_join(self) -> None:
  """Tests connecting to devices later during streaming.

  Test steps:
    1. Start audio streaming from DUT.
    2. Start advertising from REF(Left), wait for DUT to connect.
    3. Wait for audio streaming to start from REF(Left).
    4. Start advertising from REF(Right), wait for DUT to connect.
    5. Wait for audio streaming to start from REF(Right).
  """
  if self.dut.device.is_emulator:
    self.skipTest("Rootcanal doesn't support APCF.")

  # Pair and connect the major device.
  await self._pair_major_device()
  await self._pair_minor_device()

  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    for ref in self.refs:
      self.logger.info("[DUT] Disconnect REF")
      self.dut.bt.disconnect(ref.random_address)
      self.logger.info("[DUT] Wait for disconnected")
      await dut_cb.wait_for_event(
          bl4a_api.AclDisconnected(
              address=ref.random_address,
              transport=android_constants.Transport.LE,
          )
      )

  sink_ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
  for ref in self.refs:
    sink_ase_states.extend(
        pyee_extensions.EventTriggeredValueObserver(
            ase,
            ase.EVENT_STATE_CHANGE,
            functools.partial(
                lambda ase: cast(
                    ascs.AudioStreamEndpointCharacteristic, ase
                ).state,
                ase,
            ),
        )
        for ase in _get_service_from_device(
            ref.device, ascs.AudioStreamControlService
        ).ase_state_machines.values()
        if ase.role == ascs.AudioRole.SINK
    )

  self.logger.info("[DUT] Start audio streaming")
  await asyncio.to_thread(self.dut.bt.audioPlaySine)

  for sink_ase, ref in zip(sink_ase_states, self.refs):
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      await ref.device.create_advertising_set(
          advertising_parameters=_DEFAUILT_ADVERTISING_PARAMETERS,
          advertising_data=bytes(
              core.AdvertisingData([
                  ascs.make_bap_announcement(
                      announcement_type=bap.AnnouncementType.TARGETED
                  ),
                  _GENERAL_DISCOVERABLE_AD_FLAGS,
              ])
          ),
      )
      self.logger.info("[REF] Wait for ASE to be streaming")
      await sink_ase.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.State.STREAMING
      )

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/functionality/le_audio_unicast_client_dual_device_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
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.
  """
  # Pair and connect devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  sink_ase_states: list[pyee_extensions.EventTriggeredValueObserver] = []
  for ref in self.refs:
    sink_ase_states.extend(
        pyee_extensions.EventTriggeredValueObserver(
            ase,
            ase.EVENT_STATE_CHANGE,
            functools.partial(
                lambda ase: cast(
                    ascs.AudioStreamEndpointCharacteristic, ase
                ).state,
                ase,
            ),
        )
        for ase in _get_service_from_device(
            ref.device, ascs.AudioStreamControlService
        ).ase_state_machines.values()
        if ase.role == ascs.AudioRole.SINK
    )

  # Make sure audio is not streaming.
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    for ase_state in sink_ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.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):
    for ase_state in sink_ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.State.STREAMING
      )

  # 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)
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    for ase_state in sink_ase_states:
      await ase_state.wait_for_target_value(
          ascs.AudioStreamEndpointCharacteristic.State.IDLE
      )

Makes sure DUT sets the volume correctly after connecting to REF.

Source code in navi/tests/functionality/le_audio_unicast_client_dual_device_test.py
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
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")

  # Pair and connect the devices.
  await self._pair_major_device()
  await self._pair_minor_device()

  # Wait for the volume to be stable.
  await asyncio.sleep(_PREPARE_TIME_SECONDS)

  ratio = vcs.MAX_VOLUME / self.dut.bt.getMaxVolume(_StreamType.MUSIC)
  dut_volume = self.dut.bt.getVolume(_StreamType.MUSIC)
  ref_expected_volume = decimal.Decimal(dut_volume * ratio).to_integral_exact(
      rounding=decimal.ROUND_HALF_UP
  )
  # The behavior to set volume is not clear, but we can make sure the volume
  # should be correctly synchronized between DUT and all REF devices.
  for ref in self.refs:
    ref_vcs = _get_service_from_device(ref.device, vcs.VolumeControlService)
    self.assertEqual(ref_expected_volume, ref_vcs.volume_setting)

Bases: TwoDevicesTestBase

Source code in navi/tests/functionality/le_pairing_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
 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
class LePairingTest(navi_test_base.TwoDevicesTestBase):

  @retry.retry_on_exception()
  async def _make_outgoing_connection(
      self, ref_connection_address_type: _AddressType, create_bond: bool
  ) -> device.Connection:
    ref_addr = str(
        self.ref.random_address
        if ref_connection_address_type == _AddressType.RANDOM
        else self.ref.address
    )
    self.logger.info('[REF] Start advertising.')
    await self.ref.device.start_advertising(
        own_address_type=ref_connection_address_type
    )

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

      @watcher.on(self.ref.device, 'connection')
      def _(connection: device.Connection) -> None:
        if connection.transport == core.BT_LE_TRANSPORT:
          ref_dut_connection_future.set_result(connection)

      self.logger.info('[DUT] Connect to REF.')
      if create_bond:
        self.assertTrue(
            self.dut.bt.createBond(
                ref_addr,
                android_constants.Transport.LE,
                ref_connection_address_type,
            )
        )
      else:
        gatt_client = await self.dut.bl4a.connect_gatt_client(
            address=ref_addr,
            address_type=android_constants.AddressTypeStatus(
                ref_connection_address_type.value
            ),
            transport=android_constants.Transport.LE,
        )
        self.test_case_context.push(gatt_client)

      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        ref_dut_connection = await ref_dut_connection_future

      await self.ref.device.stop_advertising()
      return ref_dut_connection

  @retry.retry_on_exception()
  async def _make_incoming_connection(
      self, ref_connection_address_type: _AddressType
  ) -> device.Connection:
    # Generate a random UUID for testing.
    service_uuid = str(uuid.uuid4())

    self.logger.info(
        '[DUT] Start advertising with service UUID %s.', service_uuid
    )
    advertise = await self.dut.bl4a.start_legacy_advertiser(
        settings=bl4a_api.LegacyAdvertiseSettings(
            own_address_type=_AddressType.RANDOM
        ),
        advertising_data=bl4a_api.AdvertisingData(service_uuids=[service_uuid]),
    )

    self.logger.info('[REF] Scan for DUT.')
    scan_result = asyncio.get_running_loop().create_future()
    with advertise, pyee_extensions.EventWatcher() as watcher:

      def on_advertising_report(adv: device.Advertisement) -> None:
        if service_uuids := adv.data.get(
            core.AdvertisingData.Type.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS
        ):
          if service_uuid in service_uuids and not scan_result.done():
            scan_result.set_result(adv.address)

      watcher.on(self.ref.device, 'advertisement', on_advertising_report)
      await self.ref.device.start_scanning()
      self.logger.info(
          '[REF] Wait for advertising report(scan result) from DUT.'
      )
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        dut_addr = await scan_result
      await self.ref.device.stop_scanning()

      self.logger.info('[REF] Connect to DUT.')
      ref_dut_connection = await self.ref.device.connect(
          dut_addr,
          transport=core.BT_LE_TRANSPORT,
          own_address_type=ref_connection_address_type,
      )
      # Remote may not receive CONNECT_IND, so we need to send something to make
      # sure connection is established correctly.
      await ref_dut_connection.get_remote_le_features()

    return ref_dut_connection

  @navi_test_base.parameterized(*(
      (
          variant,
          connection_direction,
          pairing_direction,
          ref_io_capability,
          ref_connection_address_type,
          smp_key_distribution,
      )
      for (
          variant,
          connection_direction,
          pairing_direction,
          ref_io_capability,
          ref_connection_address_type,
          smp_key_distribution,
      ) in itertools.product(
          list(TestVariant),
          list(_Direction),
          list(_Direction),
          (
              pairing.PairingDelegate.NO_OUTPUT_NO_INPUT,
              pairing.PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
          ),
          (_AddressType.RANDOM, _AddressType.PUBLIC),
          (
              # IRK + LTK
              _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
              | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY,
              # IRK + LTK + LK (CTKD)
              _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
              | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
              | _KeyDistribution.DISTRIBUTE_LINK_KEY,
          ),
      )
      # Android cannot send SMP_Security_Request.
      if not (
          connection_direction == _Direction.INCOMING
          and pairing_direction == _Direction.OUTGOING
      )
  ))
  @navi_test_base.retry(max_count=2)
  async def test_secure_pairing(
      self,
      variant: TestVariant,
      connection_direction: _Direction,
      pairing_direction: _Direction,
      ref_io_capability: pairing.PairingDelegate.IoCapability,
      ref_connection_address_type: _AddressType,
      smp_key_distribution: _KeyDistribution,
  ) -> None:
    """Tests LE Secure 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.
      connection_direction: Direction of connection. DUT->REF is outgoing, and
        vice versa.
      pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
        versa.
      ref_io_capability: IO Capability on the REF device.
      ref_connection_address_type: OwnAddressType of REF used in LE-ACL.
      smp_key_distribution: Key distribution to be specified by the REF device.
    """

    # #######################
    # Setup stage
    # #######################

    pairing_delegate = pairing_utils.PairingDelegate(
        auto_accept=True,
        io_capability=ref_io_capability,
        local_initiator_key_distribution=smp_key_distribution,
        local_responder_key_distribution=smp_key_distribution,
    )

    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

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    ref_addr = str(
        self.ref.random_address
        if ref_connection_address_type == _AddressType.RANDOM
        else self.ref.address
    ).upper()

    need_double_confirmation = (
        connection_direction == _Direction.OUTGOING
        and pairing_direction == _Direction.INCOMING
    )

    # ##############################################
    # Connecting & pairing initiating stage
    # ##############################################

    ref_dut: device.Connection
    pair_task: asyncio.Task | None = None
    if connection_direction == _Direction.OUTGOING:
      if pairing_direction == _Direction.INCOMING:
        ref_dut = await self._make_outgoing_connection(
            ref_connection_address_type, create_bond=False
        )
        self.logger.info('[REF] Request pairing.')
        ref_dut.request_pairing()
      else:
        self.logger.info('[DUT] Start pairing.')
        ref_dut = await self._make_outgoing_connection(
            ref_connection_address_type, create_bond=True
        )
        # Clean all bond state events since there might be some events produced
        # by retries.
        dut_cb.get_all_events(bl4a_api.BondStateChanged)
    else:
      ref_dut = await self._make_incoming_connection(
          ref_connection_address_type
      )
      if pairing_direction == _Direction.INCOMING:
        self.logger.info('[REF] Start pairing.')
        pair_task = asyncio.create_task(ref_dut.pair())
      else:
        self.logger.info('[DUT] Start pairing.')
        self.dut.bt.createBond(
            ref_addr,
            android_constants.Transport.LE,
            ref_connection_address_type,
        )

    # #######################
    # Pairing stage
    # #######################

    self.logger.info('[DUT] Wait for pairing request.')
    dut_pairing_event = await dut_cb.wait_for_event(
        bl4a_api.PairingRequest,
        lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
    )

    if need_double_confirmation:
      self.logger.info('[DUT] Provide initial pairing confirmation.')
      self.dut.bt.setPairingConfirmation(ref_addr, True)
      self.logger.info('[DUT] Wait for 2nd pairing request.')
      dut_pairing_event = await dut_cb.wait_for_event(
          bl4a_api.PairingRequest,
          lambda e: (e.address == ref_addr),
          timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
      )

    self.logger.info('[REF] Wait for pairing request.')
    ref_pairing_event = await asyncio.wait_for(
        pairing_delegate.pairing_events.get(),
        timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
    )
    ref_answer = variant != TestVariant.REJECTED

    self.logger.info('[DUT] Check reported pairing method.')
    match ref_io_capability:
      case pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT:
        expected_dut_pairing_variant = _AndroidPairingVariant.CONSENT
        expected_ref_pairing_variant = _BumblePairingVariant.JUST_WORK
      case pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT:
        expected_dut_pairing_variant = (
            _AndroidPairingVariant.PASSKEY_CONFIRMATION
        )
        expected_ref_pairing_variant = _BumblePairingVariant.NUMERIC_COMPARISON
        self.assertEqual(ref_pairing_event.arg, dut_pairing_event.pin)
      case _:
        raise ValueError(f'Unsupported IO capability: {ref_io_capability}')

    self.assertEqual(dut_pairing_event.variant, expected_dut_pairing_variant)

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

    self.logger.info('[DUT] Handle pairing confirmation.')
    match variant:
      case TestVariant.ACCEPT | TestVariant.REJECTED:
        self.dut.bt.setPairingConfirmation(ref_addr, True)
      case TestVariant.REJECT:
        self.dut.bt.cancelBond(ref_addr)
      case _:
        # [DUT] Do nothing.
        ...

    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(
            bl4a_api.BondStateChanged,
            lambda e: (e.state in _TERMINATED_BOND_STATES),
            timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
        )
    ).state
    self.assertEqual(actual_state, expect_state)

    if pair_task:
      self.logger.info('[REF] Wait pairing complete.')
      if variant == TestVariant.ACCEPT:
        await pair_task
      else:
        with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
          await pair_task

  @navi_test_base.parameterized(*(
      (
          variant,
          connection_direction,
          pairing_direction,
          ref_io_capability,
      )
      for (
          variant,
          connection_direction,
          pairing_direction,
          ref_io_capability,
      ) in itertools.product(
          list(TestVariant),
          list(_Direction),
          list(_Direction),
          (
              pairing.PairingDelegate.NO_OUTPUT_NO_INPUT,
              pairing.PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
              pairing.PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
              pairing.PairingDelegate.DISPLAY_OUTPUT_ONLY,
              pairing.PairingDelegate.KEYBOARD_INPUT_ONLY,
          ),
      )
      # Android cannot send SMP_Security_Request.
      if not (
          connection_direction == _Direction.INCOMING
          and pairing_direction == _Direction.OUTGOING
      )
  ))
  @navi_test_base.retry(max_count=2)
  async def test_legacy_pairing(
      self,
      variant: TestVariant,
      connection_direction: _Direction,
      pairing_direction: _Direction,
      ref_io_capability: pairing.PairingDelegate.IoCapability,
  ) -> None:
    """Tests LE Secure 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.
      connection_direction: Direction of connection. DUT->REF is outgoing, and
        vice versa.
      pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
        versa.
      ref_io_capability: IO Capability on the REF device.
    """
    if (
        variant == TestVariant.REJECT
        and connection_direction == _Direction.OUTGOING
        and pairing_direction == _Direction.INCOMING
        and ref_io_capability
        in (
            pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
            pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY,
        )
        and self.dut.bluetooth_mainline_version < 361000000
    ):
      self.skipTest('This combination is broken before 2025-10 release.')

    # ####################### Setup ##########################
    pairing_delegate = pairing_utils.PairingDelegate(
        auto_accept=True,
        io_capability=ref_io_capability,
        local_initiator_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
        local_responder_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
    )

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

    self.ref.device.pairing_config_factory = pairing_config_factory

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

    need_double_confirmation = (
        connection_direction == _Direction.OUTGOING
        and pairing_direction == _Direction.INCOMING
    )

    # ####################### Connecting ##########################
    ref_dut: device.Connection
    pair_task: asyncio.Task | None = None
    if connection_direction == _Direction.OUTGOING:
      if pairing_direction == _Direction.INCOMING:
        ref_dut = await self._make_outgoing_connection(
            _AddressType.RANDOM, create_bond=False
        )
        self.logger.info('[REF] Request pairing.')
        ref_dut.request_pairing()
      else:
        self.logger.info('[DUT] Start pairing.')
        ref_dut = await self._make_outgoing_connection(
            _AddressType.RANDOM, create_bond=True
        )
        # Clean all bond state events since there might be some events produced
        # by retries.
        dut_cb.get_all_events(bl4a_api.BondStateChanged)
    else:
      ref_dut = await self._make_incoming_connection(_AddressType.RANDOM)
      if pairing_direction == _Direction.INCOMING:
        self.logger.info('[REF] Start pairing.')
        pair_task = asyncio.create_task(ref_dut.pair())
      else:
        self.logger.info('[DUT] Start pairing.')
        self.dut.bt.createBond(
            ref_addr,
            android_constants.Transport.LE,
            _AddressType.RANDOM,
        )

    # ####################### Pairing ##########################
    self.logger.info('[DUT] Wait for pairing request.')
    dut_pairing_event = await dut_cb.wait_for_event(
        bl4a_api.PairingRequest,
        lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
    )

    if need_double_confirmation:
      self.logger.info('[DUT] Provide initial pairing confirmation.')
      self.dut.bt.setPairingConfirmation(ref_addr, True)
      self.logger.info('[DUT] Wait for 2nd pairing request.')
      dut_pairing_event = await dut_cb.wait_for_event(
          bl4a_api.PairingRequest,
          lambda e: (e.address == ref_addr),
          timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
      )

    if (
        ref_io_capability
        != pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
    ):
      self.logger.info('[REF] Wait for pairing request.')
      async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
        ref_pairing_event = await pairing_delegate.pairing_events.get()
    else:
      ref_pairing_event = pairing_utils.PairingEvent(
          _BumblePairingVariant.JUST_WORK, None
      )

    dut_accept = variant != TestVariant.REJECT
    ref_accept = variant != TestVariant.REJECTED
    ref_answer: pairing_utils.PairingAnswer
    dut_answer: Callable[[], Any]

    self.logger.info('[DUT] Check reported pairing method.')
    match ref_io_capability, connection_direction:
      case (pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT, _):
        expected_dut_pairing_variant = _AndroidPairingVariant.CONSENT
        expected_ref_pairing_variant = _BumblePairingVariant.JUST_WORK
        ref_answer = ref_accept
        dut_answer = lambda: self.dut.bt.setPairingConfirmation(ref_addr, True)
      case (
          pairing.PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY,
          _,
      ) | (
          pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
          _Direction.OUTGOING,
      ):
        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
        dut_answer = lambda: self.dut.bt.setPairingConfirmation(ref_addr, True)
      case (
          pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY
          | pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
          _,
      ) | (
          pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
          _Direction.INCOMING,
      ):
        expected_dut_pairing_variant = _AndroidPairingVariant.PIN
        expected_ref_pairing_variant = (
            _BumblePairingVariant.PASSKEY_ENTRY_NOTIFICATION
        )
        ref_answer = dut_pairing_event.pin if ref_accept else None
        dut_answer = lambda: self.dut.bt.setPin(
            ref_addr, f'{ref_pairing_event.arg:06}'
        )
      case _:
        raise ValueError(f'Unsupported IO capability: {ref_io_capability}')

    self.assertEqual(dut_pairing_event.variant, expected_dut_pairing_variant)
    self.assertEqual(ref_pairing_event.variant, expected_ref_pairing_variant)

    self.logger.info('[DUT] Handle pairing confirmation.')
    if dut_accept:
      dut_answer()
    else:
      self.dut.bt.cancelBond(ref_addr)

    self.logger.info('[REF] Handle pairing confirmation.')
    match variant:
      case TestVariant.ACCEPT | TestVariant.REJECT:
        pairing_delegate.pairing_answers.put_nowait(ref_answer)
      case TestVariant.DISCONNECTED:
        await ref_dut.disconnect()
      case TestVariant.REJECTED:
        smp_session = self.ref.device.smp_manager.sessions[ref_dut.handle]
        smp_session.send_pairing_failed(smp.ErrorCode.UNSPECIFIED_REASON)  # pytype: disable=wrong-arg-types

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

    if pair_task:
      self.logger.info('[REF] Wait pairing complete.')
      if variant == TestVariant.ACCEPT:
        await pair_task
      else:
        with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
          await pair_task

  @navi_test_base.named_parameterized(*[
      dict(
          testcase_name=(
              f'{"secure" if sc else "legacy"}_{direction.name}_{address_type.name}'
              .lower()
          ),
          sc=sc,
          pairing_direction=direction,
          ref_connection_address_type=address_type,
      )
      for sc, direction, address_type in itertools.product(
          {True, False},
          {_Direction.OUTGOING, _Direction.INCOMING},
          {_AddressType.PUBLIC, _AddressType.RANDOM},
      )
      # Legacy incoming pairing is not supported on Android.
      if sc or direction == _Direction.OUTGOING
  ])
  async def test_oob_pairing(
      self,
      sc: bool,
      pairing_direction: _Direction,
      ref_connection_address_type: _AddressType,
  ) -> None:
    """Tests LE OOB pairing.

    Test steps:
      1. Setup configurations.
      2. Exchange OOB data.
      3. Start pairing.
      4. Verify final states.

    Note: Legacy variants fail from 24Q4 to 25Q3 due to stack issue.

    Args:
      sc: Whether to use secure connection.
      pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
        versa.
      ref_connection_address_type: Address type of the REF device.
    """
    if not sc and self.dut.bluetooth_mainline_version < 361000000:
      self.skipTest('Legacy OOB pairing is broken before 2025-10 release.')

    pairing_delegate = pairing_utils.PairingDelegate(
        auto_accept=True,
        local_initiator_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
        local_responder_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
    )
    ref_oob_context = pairing.OobContext()
    ref_oob_legacy_context = pairing.OobLegacyContext()
    ref_oob_config = pairing.PairingConfig.OobConfig(
        our_context=ref_oob_context,
        peer_data=None,
        legacy_context=ref_oob_legacy_context,
    )
    if ref_connection_address_type == _AddressType.RANDOM:
      ref_address_bytes = bytes(self.ref.device.random_address)
      ref_address_type = hci.AddressType.RANDOM_DEVICE
      ref_address = self.ref.random_address
    else:
      ref_address_bytes = bytes(self.ref.device.public_address)
      ref_address_type = hci.AddressType.PUBLIC_DEVICE
      ref_address = self.ref.address
    ref_address_with_type_bytes = ref_address_bytes + bytes([ref_address_type])

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

    self.ref.device.pairing_config_factory = pairing_config_factory

    dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
    self.test_case_context.push(dut_cb)
    ref_pairing_events = asyncio.Queue[None]()

    # Register a callback to get pairing events from the REF device.
    @self.ref.device.on(device.Device.EVENT_CONNECTION)
    def _(connection: device.Connection) -> None:
      connection.on(connection.EVENT_PAIRING, ref_pairing_events.put)

    if pairing_direction == _Direction.INCOMING:
      shared_data_from_dut = self.dut.bl4a.generate_oob_data(
          android_constants.Transport.LE
      )
      ref_oob_config.peer_data = pairing.OobSharedData(
          c=shared_data_from_dut.confirmation_hash,
          r=shared_data_from_dut.randomizer_hash or b'',
      )
      ref_oob_legacy_context.tk = shared_data_from_dut.le_temporary_key or b''
      ref_dut = await self._make_incoming_connection(
          ref_connection_address_type
      )
      self.logger.info('[REF] Start pairing.')
      async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
        await ref_dut.pair()
    else:
      self.logger.info('[REF] Start advertising.')
      async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
        await self.ref.device.start_advertising(
            own_address_type=ref_connection_address_type, auto_restart=False
        )

      shared_data_from_ref = ref_oob_context.share()
      dut_oob_data = bl4a_api.OobData(
          confirmation_hash=shared_data_from_ref.c,
          randomizer_hash=shared_data_from_ref.r,
          device_address_with_type=ref_address_with_type_bytes,
          le_device_role=core.LeRole.PERIPHERAL_ONLY,
          le_temporary_key=ref_oob_legacy_context.tk,
      )
      self.logger.info('[DUT] Start OOB pairing.')
      result = self.dut.bl4a.create_bond_oob(
          address=ref_address,
          address_type=(
              android_constants.AddressTypeStatus.RANDOM
              if ref_connection_address_type == _AddressType.RANDOM
              else android_constants.AddressTypeStatus.PUBLIC
          ),
          transport=android_constants.Transport.LE,
          p_192_data=dut_oob_data if not sc else None,
          p_256_data=dut_oob_data if sc else None,
      )
      self.assertTrue(result, '[DUT] Failed to create bond')

    self.logger.info('[DUT] Wait for pairing complete.')
    bonded_event = await dut_cb.wait_for_event(
        bl4a_api.BondStateChanged(
            address=ref_address, state=matcher.any_of(*_TERMINATED_BOND_STATES)
        )
    )
    self.assertEqual(bonded_event.state, android_constants.BondState.BONDED)

    self.logger.info('[REF] Wait for pairing complete.')
    async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
      await ref_pairing_events.get()

Tests LE Secure 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.

Parameters:

Name Type Description Default
variant TestVariant

Action to perform in the pairing procedure.

required
connection_direction _Direction

Direction of connection. DUT->REF is outgoing, and vice versa.

required
pairing_direction _Direction

Direction of pairing. DUT->REF is outgoing, and vice versa.

required
ref_io_capability IoCapability

IO Capability on the REF device.

required
Source code in navi/tests/functionality/le_pairing_test.py
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
@navi_test_base.parameterized(*(
    (
        variant,
        connection_direction,
        pairing_direction,
        ref_io_capability,
    )
    for (
        variant,
        connection_direction,
        pairing_direction,
        ref_io_capability,
    ) in itertools.product(
        list(TestVariant),
        list(_Direction),
        list(_Direction),
        (
            pairing.PairingDelegate.NO_OUTPUT_NO_INPUT,
            pairing.PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
            pairing.PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
            pairing.PairingDelegate.DISPLAY_OUTPUT_ONLY,
            pairing.PairingDelegate.KEYBOARD_INPUT_ONLY,
        ),
    )
    # Android cannot send SMP_Security_Request.
    if not (
        connection_direction == _Direction.INCOMING
        and pairing_direction == _Direction.OUTGOING
    )
))
@navi_test_base.retry(max_count=2)
async def test_legacy_pairing(
    self,
    variant: TestVariant,
    connection_direction: _Direction,
    pairing_direction: _Direction,
    ref_io_capability: pairing.PairingDelegate.IoCapability,
) -> None:
  """Tests LE Secure 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.
    connection_direction: Direction of connection. DUT->REF is outgoing, and
      vice versa.
    pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
      versa.
    ref_io_capability: IO Capability on the REF device.
  """
  if (
      variant == TestVariant.REJECT
      and connection_direction == _Direction.OUTGOING
      and pairing_direction == _Direction.INCOMING
      and ref_io_capability
      in (
          pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
          pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY,
      )
      and self.dut.bluetooth_mainline_version < 361000000
  ):
    self.skipTest('This combination is broken before 2025-10 release.')

  # ####################### Setup ##########################
  pairing_delegate = pairing_utils.PairingDelegate(
      auto_accept=True,
      io_capability=ref_io_capability,
      local_initiator_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
      local_responder_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
  )

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

  self.ref.device.pairing_config_factory = pairing_config_factory

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

  need_double_confirmation = (
      connection_direction == _Direction.OUTGOING
      and pairing_direction == _Direction.INCOMING
  )

  # ####################### Connecting ##########################
  ref_dut: device.Connection
  pair_task: asyncio.Task | None = None
  if connection_direction == _Direction.OUTGOING:
    if pairing_direction == _Direction.INCOMING:
      ref_dut = await self._make_outgoing_connection(
          _AddressType.RANDOM, create_bond=False
      )
      self.logger.info('[REF] Request pairing.')
      ref_dut.request_pairing()
    else:
      self.logger.info('[DUT] Start pairing.')
      ref_dut = await self._make_outgoing_connection(
          _AddressType.RANDOM, create_bond=True
      )
      # Clean all bond state events since there might be some events produced
      # by retries.
      dut_cb.get_all_events(bl4a_api.BondStateChanged)
  else:
    ref_dut = await self._make_incoming_connection(_AddressType.RANDOM)
    if pairing_direction == _Direction.INCOMING:
      self.logger.info('[REF] Start pairing.')
      pair_task = asyncio.create_task(ref_dut.pair())
    else:
      self.logger.info('[DUT] Start pairing.')
      self.dut.bt.createBond(
          ref_addr,
          android_constants.Transport.LE,
          _AddressType.RANDOM,
      )

  # ####################### Pairing ##########################
  self.logger.info('[DUT] Wait for pairing request.')
  dut_pairing_event = await dut_cb.wait_for_event(
      bl4a_api.PairingRequest,
      lambda e: (e.address == ref_addr),
      timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
  )

  if need_double_confirmation:
    self.logger.info('[DUT] Provide initial pairing confirmation.')
    self.dut.bt.setPairingConfirmation(ref_addr, True)
    self.logger.info('[DUT] Wait for 2nd pairing request.')
    dut_pairing_event = await dut_cb.wait_for_event(
        bl4a_api.PairingRequest,
        lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
    )

  if (
      ref_io_capability
      != pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
  ):
    self.logger.info('[REF] Wait for pairing request.')
    async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
      ref_pairing_event = await pairing_delegate.pairing_events.get()
  else:
    ref_pairing_event = pairing_utils.PairingEvent(
        _BumblePairingVariant.JUST_WORK, None
    )

  dut_accept = variant != TestVariant.REJECT
  ref_accept = variant != TestVariant.REJECTED
  ref_answer: pairing_utils.PairingAnswer
  dut_answer: Callable[[], Any]

  self.logger.info('[DUT] Check reported pairing method.')
  match ref_io_capability, connection_direction:
    case (pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT, _):
      expected_dut_pairing_variant = _AndroidPairingVariant.CONSENT
      expected_ref_pairing_variant = _BumblePairingVariant.JUST_WORK
      ref_answer = ref_accept
      dut_answer = lambda: self.dut.bt.setPairingConfirmation(ref_addr, True)
    case (
        pairing.PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY,
        _,
    ) | (
        pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
        _Direction.OUTGOING,
    ):
      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
      dut_answer = lambda: self.dut.bt.setPairingConfirmation(ref_addr, True)
    case (
        pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY
        | pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        _,
    ) | (
        pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
        _Direction.INCOMING,
    ):
      expected_dut_pairing_variant = _AndroidPairingVariant.PIN
      expected_ref_pairing_variant = (
          _BumblePairingVariant.PASSKEY_ENTRY_NOTIFICATION
      )
      ref_answer = dut_pairing_event.pin if ref_accept else None
      dut_answer = lambda: self.dut.bt.setPin(
          ref_addr, f'{ref_pairing_event.arg:06}'
      )
    case _:
      raise ValueError(f'Unsupported IO capability: {ref_io_capability}')

  self.assertEqual(dut_pairing_event.variant, expected_dut_pairing_variant)
  self.assertEqual(ref_pairing_event.variant, expected_ref_pairing_variant)

  self.logger.info('[DUT] Handle pairing confirmation.')
  if dut_accept:
    dut_answer()
  else:
    self.dut.bt.cancelBond(ref_addr)

  self.logger.info('[REF] Handle pairing confirmation.')
  match variant:
    case TestVariant.ACCEPT | TestVariant.REJECT:
      pairing_delegate.pairing_answers.put_nowait(ref_answer)
    case TestVariant.DISCONNECTED:
      await ref_dut.disconnect()
    case TestVariant.REJECTED:
      smp_session = self.ref.device.smp_manager.sessions[ref_dut.handle]
      smp_session.send_pairing_failed(smp.ErrorCode.UNSPECIFIED_REASON)  # pytype: disable=wrong-arg-types

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

  if pair_task:
    self.logger.info('[REF] Wait pairing complete.')
    if variant == TestVariant.ACCEPT:
      await pair_task
    else:
      with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
        await pair_task

Tests LE OOB pairing.

Test steps
  1. Setup configurations.
  2. Exchange OOB data.
  3. Start pairing.
  4. Verify final states.

Note: Legacy variants fail from 24Q4 to 25Q3 due to stack issue.

Parameters:

Name Type Description Default
sc bool

Whether to use secure connection.

required
pairing_direction _Direction

Direction of pairing. DUT->REF is outgoing, and vice versa.

required
ref_connection_address_type _AddressType

Address type of the REF device.

required
Source code in navi/tests/functionality/le_pairing_test.py
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
@navi_test_base.named_parameterized(*[
    dict(
        testcase_name=(
            f'{"secure" if sc else "legacy"}_{direction.name}_{address_type.name}'
            .lower()
        ),
        sc=sc,
        pairing_direction=direction,
        ref_connection_address_type=address_type,
    )
    for sc, direction, address_type in itertools.product(
        {True, False},
        {_Direction.OUTGOING, _Direction.INCOMING},
        {_AddressType.PUBLIC, _AddressType.RANDOM},
    )
    # Legacy incoming pairing is not supported on Android.
    if sc or direction == _Direction.OUTGOING
])
async def test_oob_pairing(
    self,
    sc: bool,
    pairing_direction: _Direction,
    ref_connection_address_type: _AddressType,
) -> None:
  """Tests LE OOB pairing.

  Test steps:
    1. Setup configurations.
    2. Exchange OOB data.
    3. Start pairing.
    4. Verify final states.

  Note: Legacy variants fail from 24Q4 to 25Q3 due to stack issue.

  Args:
    sc: Whether to use secure connection.
    pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
      versa.
    ref_connection_address_type: Address type of the REF device.
  """
  if not sc and self.dut.bluetooth_mainline_version < 361000000:
    self.skipTest('Legacy OOB pairing is broken before 2025-10 release.')

  pairing_delegate = pairing_utils.PairingDelegate(
      auto_accept=True,
      local_initiator_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
      local_responder_key_distribution=pairing.PairingDelegate.DEFAULT_KEY_DISTRIBUTION,
  )
  ref_oob_context = pairing.OobContext()
  ref_oob_legacy_context = pairing.OobLegacyContext()
  ref_oob_config = pairing.PairingConfig.OobConfig(
      our_context=ref_oob_context,
      peer_data=None,
      legacy_context=ref_oob_legacy_context,
  )
  if ref_connection_address_type == _AddressType.RANDOM:
    ref_address_bytes = bytes(self.ref.device.random_address)
    ref_address_type = hci.AddressType.RANDOM_DEVICE
    ref_address = self.ref.random_address
  else:
    ref_address_bytes = bytes(self.ref.device.public_address)
    ref_address_type = hci.AddressType.PUBLIC_DEVICE
    ref_address = self.ref.address
  ref_address_with_type_bytes = ref_address_bytes + bytes([ref_address_type])

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

  self.ref.device.pairing_config_factory = pairing_config_factory

  dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(dut_cb)
  ref_pairing_events = asyncio.Queue[None]()

  # Register a callback to get pairing events from the REF device.
  @self.ref.device.on(device.Device.EVENT_CONNECTION)
  def _(connection: device.Connection) -> None:
    connection.on(connection.EVENT_PAIRING, ref_pairing_events.put)

  if pairing_direction == _Direction.INCOMING:
    shared_data_from_dut = self.dut.bl4a.generate_oob_data(
        android_constants.Transport.LE
    )
    ref_oob_config.peer_data = pairing.OobSharedData(
        c=shared_data_from_dut.confirmation_hash,
        r=shared_data_from_dut.randomizer_hash or b'',
    )
    ref_oob_legacy_context.tk = shared_data_from_dut.le_temporary_key or b''
    ref_dut = await self._make_incoming_connection(
        ref_connection_address_type
    )
    self.logger.info('[REF] Start pairing.')
    async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
      await ref_dut.pair()
  else:
    self.logger.info('[REF] Start advertising.')
    async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
      await self.ref.device.start_advertising(
          own_address_type=ref_connection_address_type, auto_restart=False
      )

    shared_data_from_ref = ref_oob_context.share()
    dut_oob_data = bl4a_api.OobData(
        confirmation_hash=shared_data_from_ref.c,
        randomizer_hash=shared_data_from_ref.r,
        device_address_with_type=ref_address_with_type_bytes,
        le_device_role=core.LeRole.PERIPHERAL_ONLY,
        le_temporary_key=ref_oob_legacy_context.tk,
    )
    self.logger.info('[DUT] Start OOB pairing.')
    result = self.dut.bl4a.create_bond_oob(
        address=ref_address,
        address_type=(
            android_constants.AddressTypeStatus.RANDOM
            if ref_connection_address_type == _AddressType.RANDOM
            else android_constants.AddressTypeStatus.PUBLIC
        ),
        transport=android_constants.Transport.LE,
        p_192_data=dut_oob_data if not sc else None,
        p_256_data=dut_oob_data if sc else None,
    )
    self.assertTrue(result, '[DUT] Failed to create bond')

  self.logger.info('[DUT] Wait for pairing complete.')
  bonded_event = await dut_cb.wait_for_event(
      bl4a_api.BondStateChanged(
          address=ref_address, state=matcher.any_of(*_TERMINATED_BOND_STATES)
      )
  )
  self.assertEqual(bonded_event.state, android_constants.BondState.BONDED)

  self.logger.info('[REF] Wait for pairing complete.')
  async with self.assert_not_timeout(_DEFAULT_SETUP_TIMEOUT_SECONDS):
    await ref_pairing_events.get()

Tests LE Secure 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.

Parameters:

Name Type Description Default
variant TestVariant

Action to perform in the pairing procedure.

required
connection_direction _Direction

Direction of connection. DUT->REF is outgoing, and vice versa.

required
pairing_direction _Direction

Direction of pairing. DUT->REF is outgoing, and vice versa.

required
ref_io_capability IoCapability

IO Capability on the REF device.

required
ref_connection_address_type _AddressType

OwnAddressType of REF used in LE-ACL.

required
smp_key_distribution _KeyDistribution

Key distribution to be specified by the REF device.

required
Source code in navi/tests/functionality/le_pairing_test.py
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
@navi_test_base.parameterized(*(
    (
        variant,
        connection_direction,
        pairing_direction,
        ref_io_capability,
        ref_connection_address_type,
        smp_key_distribution,
    )
    for (
        variant,
        connection_direction,
        pairing_direction,
        ref_io_capability,
        ref_connection_address_type,
        smp_key_distribution,
    ) in itertools.product(
        list(TestVariant),
        list(_Direction),
        list(_Direction),
        (
            pairing.PairingDelegate.NO_OUTPUT_NO_INPUT,
            pairing.PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
        ),
        (_AddressType.RANDOM, _AddressType.PUBLIC),
        (
            # IRK + LTK
            _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
            | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY,
            # IRK + LTK + LK (CTKD)
            _KeyDistribution.DISTRIBUTE_ENCRYPTION_KEY
            | _KeyDistribution.DISTRIBUTE_IDENTITY_KEY
            | _KeyDistribution.DISTRIBUTE_LINK_KEY,
        ),
    )
    # Android cannot send SMP_Security_Request.
    if not (
        connection_direction == _Direction.INCOMING
        and pairing_direction == _Direction.OUTGOING
    )
))
@navi_test_base.retry(max_count=2)
async def test_secure_pairing(
    self,
    variant: TestVariant,
    connection_direction: _Direction,
    pairing_direction: _Direction,
    ref_io_capability: pairing.PairingDelegate.IoCapability,
    ref_connection_address_type: _AddressType,
    smp_key_distribution: _KeyDistribution,
) -> None:
  """Tests LE Secure 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.
    connection_direction: Direction of connection. DUT->REF is outgoing, and
      vice versa.
    pairing_direction: Direction of pairing. DUT->REF is outgoing, and vice
      versa.
    ref_io_capability: IO Capability on the REF device.
    ref_connection_address_type: OwnAddressType of REF used in LE-ACL.
    smp_key_distribution: Key distribution to be specified by the REF device.
  """

  # #######################
  # Setup stage
  # #######################

  pairing_delegate = pairing_utils.PairingDelegate(
      auto_accept=True,
      io_capability=ref_io_capability,
      local_initiator_key_distribution=smp_key_distribution,
      local_responder_key_distribution=smp_key_distribution,
  )

  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

  dut_cb = self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER)
  self.test_case_context.push(dut_cb)
  ref_addr = str(
      self.ref.random_address
      if ref_connection_address_type == _AddressType.RANDOM
      else self.ref.address
  ).upper()

  need_double_confirmation = (
      connection_direction == _Direction.OUTGOING
      and pairing_direction == _Direction.INCOMING
  )

  # ##############################################
  # Connecting & pairing initiating stage
  # ##############################################

  ref_dut: device.Connection
  pair_task: asyncio.Task | None = None
  if connection_direction == _Direction.OUTGOING:
    if pairing_direction == _Direction.INCOMING:
      ref_dut = await self._make_outgoing_connection(
          ref_connection_address_type, create_bond=False
      )
      self.logger.info('[REF] Request pairing.')
      ref_dut.request_pairing()
    else:
      self.logger.info('[DUT] Start pairing.')
      ref_dut = await self._make_outgoing_connection(
          ref_connection_address_type, create_bond=True
      )
      # Clean all bond state events since there might be some events produced
      # by retries.
      dut_cb.get_all_events(bl4a_api.BondStateChanged)
  else:
    ref_dut = await self._make_incoming_connection(
        ref_connection_address_type
    )
    if pairing_direction == _Direction.INCOMING:
      self.logger.info('[REF] Start pairing.')
      pair_task = asyncio.create_task(ref_dut.pair())
    else:
      self.logger.info('[DUT] Start pairing.')
      self.dut.bt.createBond(
          ref_addr,
          android_constants.Transport.LE,
          ref_connection_address_type,
      )

  # #######################
  # Pairing stage
  # #######################

  self.logger.info('[DUT] Wait for pairing request.')
  dut_pairing_event = await dut_cb.wait_for_event(
      bl4a_api.PairingRequest,
      lambda e: (e.address == ref_addr),
      timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
  )

  if need_double_confirmation:
    self.logger.info('[DUT] Provide initial pairing confirmation.')
    self.dut.bt.setPairingConfirmation(ref_addr, True)
    self.logger.info('[DUT] Wait for 2nd pairing request.')
    dut_pairing_event = await dut_cb.wait_for_event(
        bl4a_api.PairingRequest,
        lambda e: (e.address == ref_addr),
        timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
    )

  self.logger.info('[REF] Wait for pairing request.')
  ref_pairing_event = await asyncio.wait_for(
      pairing_delegate.pairing_events.get(),
      timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
  )
  ref_answer = variant != TestVariant.REJECTED

  self.logger.info('[DUT] Check reported pairing method.')
  match ref_io_capability:
    case pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT:
      expected_dut_pairing_variant = _AndroidPairingVariant.CONSENT
      expected_ref_pairing_variant = _BumblePairingVariant.JUST_WORK
    case pairing.PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT:
      expected_dut_pairing_variant = (
          _AndroidPairingVariant.PASSKEY_CONFIRMATION
      )
      expected_ref_pairing_variant = _BumblePairingVariant.NUMERIC_COMPARISON
      self.assertEqual(ref_pairing_event.arg, dut_pairing_event.pin)
    case _:
      raise ValueError(f'Unsupported IO capability: {ref_io_capability}')

  self.assertEqual(dut_pairing_event.variant, expected_dut_pairing_variant)

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

  self.logger.info('[DUT] Handle pairing confirmation.')
  match variant:
    case TestVariant.ACCEPT | TestVariant.REJECTED:
      self.dut.bt.setPairingConfirmation(ref_addr, True)
    case TestVariant.REJECT:
      self.dut.bt.cancelBond(ref_addr)
    case _:
      # [DUT] Do nothing.
      ...

  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(
          bl4a_api.BondStateChanged,
          lambda e: (e.state in _TERMINATED_BOND_STATES),
          timeout=_DEFAULT_SETUP_TIMEOUT_SECONDS,
      )
  ).state
  self.assertEqual(actual_state, expect_state)

  if pair_task:
    self.logger.info('[REF] Wait pairing complete.')
    if variant == TestVariant.ACCEPT:
      await pair_task
    else:
      with self.assertRaises((core.ProtocolError, asyncio.CancelledError)):
        await pair_task

Bases: TwoDevicesTestBase

Source code in navi/tests/functionality/rfcomm_socket_test.py
 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
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
class RfcommSocketTest(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()

  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())

  def _setup_rfcomm_server_on_ref(
      self,
      service_record_handle: int,
      rfcomm_uuid: str,
      rfcomm_server: rfcomm.Server,
  ) -> asyncio.Queue[rfcomm.DLC]:
    """Sets up RFCOMM server on REF and returns a queue for its result.

    Args:
      service_record_handle: The service record handle for the RFCOMM server.
      rfcomm_uuid: The UUID of the RFCOMM server.
      rfcomm_server: The RFCOMM server to listen on.

    Returns:
      An asyncio queue for the RFCOMM server's incoming DLC.
    """
    accept_queue = asyncio.Queue[rfcomm.DLC](maxsize=1)
    rfcomm_channel = rfcomm_server.listen(
        acceptor=accept_queue.put_nowait,
    )
    self.logger.info(
        "[REF] Create RFCOMM socket server with rfcomm_uuid %s.",
        rfcomm_uuid,
    )

    self.ref.device.sdp_service_records[service_record_handle] = (
        rfcomm.make_service_sdp_records(
            service_record_handle=service_record_handle,
            channel=rfcomm_channel,
            uuid=core.UUID(rfcomm_uuid),
        )
    )

    return accept_queue

  @navi_test_base.parameterized(1, 2)
  async def test_rfcomm_socket_connections_simultaneously(
      self, num_connections: int
  ) -> None:
    """Tests one or two RFCOMM socket connections simultaneously.

    Typical duration: 30-50s.

    Test steps:
      1. Create RFCOMM sockets server on REF.
      2. Pair DUT and REF.
      3. Create RFCOMM sockets connection from DUT to REF.
      4. Verify RFCOMM sockets connection are successful.

    Args:
      num_connections: The number of RFCOMM socket connections to create.
    """
    # Initialize RFCOMM sockets server on REF.
    rfcomm_server = rfcomm.Server(self.ref.device)

    # Create RFCOMM sockets server on REF.
    rfcomm_uuid_list = [str(uuid.uuid4()) for _ in range(num_connections)]
    ref_accept_queues = [
        self._setup_rfcomm_server_on_ref(i, rfcomm_uuid, rfcomm_server)
        for i, rfcomm_uuid in enumerate(rfcomm_uuid_list)
    ]

    # Pair DUT and REF.
    await self._setup_pairing()

    # Create RFCOMM sockets connection from DUT to REF.
    rfcomm_sockets = [
        self.dut.bl4a.create_rfcomm_channel_async(
            address=self.ref.address,
            secure=True,
            uuid=rfcomm_uuid,
        )
        for rfcomm_uuid in rfcomm_uuid_list
    ]

    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      # Wait for all RFCOMM sockets connection to complete, and get the results.
      self.logger.info("[REF] Wait for all RFCOMM connections to be accepted.")
      server_accept_results = await asyncio.gather(
          *[q.get() for q in ref_accept_queues]
      )
      self.logger.info("[DUT] Wait for all RFCOMM connections to complete.")
      await asyncio.gather(*[
          rfcomm_socket.wait_for_connected() for rfcomm_socket in rfcomm_sockets
      ])
      self.logger.info("[DUT] All RFCOMM connections completed.")

      # Verify both RFCOMM sockets connection are successful.
      for dlc_result in server_accept_results:
        self.logger.info("dlc_result: %s", dlc_result)
        self.assertEqual(
            dlc_result.state,
            rfcomm.DLC.State.CONNECTED,
            "DLC connection failed. Expected state: CONNECTED, but got:"
            f" {dlc_result.state.name}",
        )

  async def test_concurrent_rfcomm_connect_fail_raises_exception(
      self,
  ) -> None:
    """Tests concurrent RFCOMM connect fail should raises exception.

    Typical duration: 30-60s.

    Test steps:
      1. Create TWO RFCOMM sockets server on REF.
      2. Connect TWO RFCOMM sockets from DUT to REF at the same time.
      3. Reject the Rfcomm connection request on REF by l2cap connection
      request with No resources available.
      4. Verify the DUT can catch the exceptions raised for both RFCOMM
      connections .
    """
    original_on_l2cap_connection_request = (
        self.ref.device.l2cap_channel_manager.on_l2cap_connection_request
    )

    def custom_on_l2cap_connection_request(
        connection: device.Connection,
        cid: int,
        request: l2cap.L2CAP_Connection_Request,
    ) -> None:
      self.logger.info(
          " _custom_on_l2cap_connection_request:: psm: %s", request.psm
      )

      if request.psm == rfcomm.RFCOMM_PSM:
        self.logger.info(" RFCOMM L2CAP connection request rejected")
        self.ref.device.l2cap_channel_manager.send_control_frame(
            connection,
            cid,
            l2cap.L2CAP_Connection_Response(
                identifier=request.identifier,
                destination_cid=0,
                source_cid=request.source_cid,
                result=l2cap.L2CAP_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
                status=0x0000,
            ),
        )
      else:
        original_on_l2cap_connection_request(connection, cid, request)

    # Replace the original on_l2cap_connection_request with the custom one.
    self.ref.device.l2cap_channel_manager.on_l2cap_connection_request = (
        custom_on_l2cap_connection_request
    )

    ref_accept_future = asyncio.get_running_loop().create_future()
    rfcomm_sockets: list[bl4a_api.RfcommChannel] = []

    rfcomm_server = rfcomm.Server(self.ref.device)
    for i in range(2):
      # Create RFCOMM sockets server on REF.
      rfcomm_channel = rfcomm_server.listen(
          acceptor=ref_accept_future.set_result,
      )
      rfcomm_uuid = str(uuid.uuid4())
      self.logger.info(
          "[REF] Create %d RFCOMM socket server with rfcomm_uuid %s.",
          i,
          rfcomm_uuid,
      )
      self.ref.device.sdp_service_records[i] = rfcomm.make_service_sdp_records(
          service_record_handle=i,
          channel=rfcomm_channel,
          uuid=core.UUID(rfcomm_uuid),
      )

      # Create RFCOMM socket connection from DUT to REF.
      rfcomm_sockets.append(
          self.dut.bl4a.create_rfcomm_channel_async(
              address=self.ref.address,
              secure=True,
              uuid=rfcomm_uuid,
          )
      )

    # Await the pairing request from DUT and accept the request.
    with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
      self.logger.info("[DUT] Wait for pairing request.")
      await dut_cb.wait_for_event(
          bl4a_api.PairingRequest(
              address=self.ref.address, variant=mock.ANY, pin=mock.ANY
          ),
          timeout=10,
      )

      # Wait for 5 seconds for:
      #   1. Simulate user interaction delay with a pop-up.
      #   2. Ensures both RFCOMM sockets complete SDP, creating an L2CAP
      #     connection collision.
      #   3. RFCOMM L2CAP connection request will be pending due to incomplete
      #     encryption.
      self.logger.info("[DUT] setPairingConfirmation Wait for 5 seconds.")
      await asyncio.sleep(_DEFAULT_STEP_TIMEOUT_SECONDS)
      self.assertTrue(
          self.dut.bt.setPairingConfirmation(self.ref.address, True)
      )

      # wait for both RFCOMM sockets connection to fail.
      async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
        for rfcomm_socket in rfcomm_sockets:
          with self.assertRaises(errors.ConnectionError):
            await rfcomm_socket.wait_for_connected()

  async def test_rfcomm_connect_after_page_timeout(self) -> None:
    """Tests RFCOMM connect after page timeout.

    Typical duration: 30-60s.

    Test steps:
      1. Pair DUT and REF.
      2. Disable inquiry and page scan of REF.
      4. Create and connect to an RFCOMM socket - expect not connected because
      of REF non-connectable.
      5. Wait 3 seconds.
      6. Before page timeout of 5 seconds, close the socket.
      7. Enable inquiry and page scan of REF.
      8. Create and connect to an RFCOMM socket - verify proper should be
      connected.
    """
    # Pair DUT and REF.
    await self._setup_pairing()

    # Set REF to be non-connectable and non-discoverable.
    self.logger.info("[REF] Setting device to be non-connectable.")
    await self.ref.device.set_discoverable(False)
    await self.ref.device.set_connectable(False)

    # Create RFCOMM sockets server on REF.
    rfcomm_server = rfcomm.Server(self.ref.device)
    rfcomm_uuid = str(uuid.uuid4())
    accept_queue = self._setup_rfcomm_server_on_ref(
        0, rfcomm_uuid, rfcomm_server
    )

    self.logger.info(
        "[DUT] Attempting to connect to non-connectable REF (expecting to"
        " hang)."
    )
    rfcomm_socket = self.dut.bl4a.create_rfcomm_channel_async(
        address=self.ref.address,
        secure=False,
        uuid=rfcomm_uuid,
    )

    # For Android device, the page timeout is 5 seconds.
    # Wait for 3 seconds before page timeout of 5 seconds
    await asyncio.sleep(_PENDING_CONNECTION_WAIT_SECONDS)

    # Close the RFCOMM socket.
    await rfcomm_socket.close()

    # Set REF to be connectable and discoverable.
    self.logger.info("[REF] Setting device to be connectable and discoverable.")
    await self.ref.device.set_discoverable(True)
    await self.ref.device.set_connectable(True)

    self.logger.info("[DUT] Connect RFCOMM channel to REF.")
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS * 2):
      ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
          accept_queue.get(),
          self.dut.bl4a.create_rfcomm_channel(
              address=self.ref.address,
              secure=False,
              uuid=rfcomm_uuid,
          ),
      )

    self.logger.info("[DUT] Verify RFCOMM channel is connected.")
    self.assertEqual(
        ref_dut_dlc.state,
        rfcomm.DLC.State.CONNECTED,
        "DLC connection failed. Expected state: CONNECTED, but got:"
        f" {ref_dut_dlc.state.name}",
    )

    self.logger.info("[DUT] Disconnect RFCOMM channel.")
    await dut_ref_dlc.close()

Tests concurrent RFCOMM connect fail should raises exception.

Typical duration: 30-60s.

Test steps
  1. Create TWO RFCOMM sockets server on REF.
  2. Connect TWO RFCOMM sockets from DUT to REF at the same time.
  3. Reject the Rfcomm connection request on REF by l2cap connection request with No resources available.
  4. Verify the DUT can catch the exceptions raised for both RFCOMM connections .
Source code in navi/tests/functionality/rfcomm_socket_test.py
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
async def test_concurrent_rfcomm_connect_fail_raises_exception(
    self,
) -> None:
  """Tests concurrent RFCOMM connect fail should raises exception.

  Typical duration: 30-60s.

  Test steps:
    1. Create TWO RFCOMM sockets server on REF.
    2. Connect TWO RFCOMM sockets from DUT to REF at the same time.
    3. Reject the Rfcomm connection request on REF by l2cap connection
    request with No resources available.
    4. Verify the DUT can catch the exceptions raised for both RFCOMM
    connections .
  """
  original_on_l2cap_connection_request = (
      self.ref.device.l2cap_channel_manager.on_l2cap_connection_request
  )

  def custom_on_l2cap_connection_request(
      connection: device.Connection,
      cid: int,
      request: l2cap.L2CAP_Connection_Request,
  ) -> None:
    self.logger.info(
        " _custom_on_l2cap_connection_request:: psm: %s", request.psm
    )

    if request.psm == rfcomm.RFCOMM_PSM:
      self.logger.info(" RFCOMM L2CAP connection request rejected")
      self.ref.device.l2cap_channel_manager.send_control_frame(
          connection,
          cid,
          l2cap.L2CAP_Connection_Response(
              identifier=request.identifier,
              destination_cid=0,
              source_cid=request.source_cid,
              result=l2cap.L2CAP_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
              status=0x0000,
          ),
      )
    else:
      original_on_l2cap_connection_request(connection, cid, request)

  # Replace the original on_l2cap_connection_request with the custom one.
  self.ref.device.l2cap_channel_manager.on_l2cap_connection_request = (
      custom_on_l2cap_connection_request
  )

  ref_accept_future = asyncio.get_running_loop().create_future()
  rfcomm_sockets: list[bl4a_api.RfcommChannel] = []

  rfcomm_server = rfcomm.Server(self.ref.device)
  for i in range(2):
    # Create RFCOMM sockets server on REF.
    rfcomm_channel = rfcomm_server.listen(
        acceptor=ref_accept_future.set_result,
    )
    rfcomm_uuid = str(uuid.uuid4())
    self.logger.info(
        "[REF] Create %d RFCOMM socket server with rfcomm_uuid %s.",
        i,
        rfcomm_uuid,
    )
    self.ref.device.sdp_service_records[i] = rfcomm.make_service_sdp_records(
        service_record_handle=i,
        channel=rfcomm_channel,
        uuid=core.UUID(rfcomm_uuid),
    )

    # Create RFCOMM socket connection from DUT to REF.
    rfcomm_sockets.append(
        self.dut.bl4a.create_rfcomm_channel_async(
            address=self.ref.address,
            secure=True,
            uuid=rfcomm_uuid,
        )
    )

  # Await the pairing request from DUT and accept the request.
  with self.dut.bl4a.register_callback(bl4a_api.Module.ADAPTER) as dut_cb:
    self.logger.info("[DUT] Wait for pairing request.")
    await dut_cb.wait_for_event(
        bl4a_api.PairingRequest(
            address=self.ref.address, variant=mock.ANY, pin=mock.ANY
        ),
        timeout=10,
    )

    # Wait for 5 seconds for:
    #   1. Simulate user interaction delay with a pop-up.
    #   2. Ensures both RFCOMM sockets complete SDP, creating an L2CAP
    #     connection collision.
    #   3. RFCOMM L2CAP connection request will be pending due to incomplete
    #     encryption.
    self.logger.info("[DUT] setPairingConfirmation Wait for 5 seconds.")
    await asyncio.sleep(_DEFAULT_STEP_TIMEOUT_SECONDS)
    self.assertTrue(
        self.dut.bt.setPairingConfirmation(self.ref.address, True)
    )

    # wait for both RFCOMM sockets connection to fail.
    async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
      for rfcomm_socket in rfcomm_sockets:
        with self.assertRaises(errors.ConnectionError):
          await rfcomm_socket.wait_for_connected()

Tests RFCOMM connect after page timeout.

Typical duration: 30-60s.

Test steps
  1. Pair DUT and REF.
  2. Disable inquiry and page scan of REF.
  3. Create and connect to an RFCOMM socket - expect not connected because of REF non-connectable.
  4. Wait 3 seconds.
  5. Before page timeout of 5 seconds, close the socket.
  6. Enable inquiry and page scan of REF.
  7. Create and connect to an RFCOMM socket - verify proper should be connected.
Source code in navi/tests/functionality/rfcomm_socket_test.py
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
async def test_rfcomm_connect_after_page_timeout(self) -> None:
  """Tests RFCOMM connect after page timeout.

  Typical duration: 30-60s.

  Test steps:
    1. Pair DUT and REF.
    2. Disable inquiry and page scan of REF.
    4. Create and connect to an RFCOMM socket - expect not connected because
    of REF non-connectable.
    5. Wait 3 seconds.
    6. Before page timeout of 5 seconds, close the socket.
    7. Enable inquiry and page scan of REF.
    8. Create and connect to an RFCOMM socket - verify proper should be
    connected.
  """
  # Pair DUT and REF.
  await self._setup_pairing()

  # Set REF to be non-connectable and non-discoverable.
  self.logger.info("[REF] Setting device to be non-connectable.")
  await self.ref.device.set_discoverable(False)
  await self.ref.device.set_connectable(False)

  # Create RFCOMM sockets server on REF.
  rfcomm_server = rfcomm.Server(self.ref.device)
  rfcomm_uuid = str(uuid.uuid4())
  accept_queue = self._setup_rfcomm_server_on_ref(
      0, rfcomm_uuid, rfcomm_server
  )

  self.logger.info(
      "[DUT] Attempting to connect to non-connectable REF (expecting to"
      " hang)."
  )
  rfcomm_socket = self.dut.bl4a.create_rfcomm_channel_async(
      address=self.ref.address,
      secure=False,
      uuid=rfcomm_uuid,
  )

  # For Android device, the page timeout is 5 seconds.
  # Wait for 3 seconds before page timeout of 5 seconds
  await asyncio.sleep(_PENDING_CONNECTION_WAIT_SECONDS)

  # Close the RFCOMM socket.
  await rfcomm_socket.close()

  # Set REF to be connectable and discoverable.
  self.logger.info("[REF] Setting device to be connectable and discoverable.")
  await self.ref.device.set_discoverable(True)
  await self.ref.device.set_connectable(True)

  self.logger.info("[DUT] Connect RFCOMM channel to REF.")
  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS * 2):
    ref_dut_dlc, dut_ref_dlc = await asyncio.gather(
        accept_queue.get(),
        self.dut.bl4a.create_rfcomm_channel(
            address=self.ref.address,
            secure=False,
            uuid=rfcomm_uuid,
        ),
    )

  self.logger.info("[DUT] Verify RFCOMM channel is connected.")
  self.assertEqual(
      ref_dut_dlc.state,
      rfcomm.DLC.State.CONNECTED,
      "DLC connection failed. Expected state: CONNECTED, but got:"
      f" {ref_dut_dlc.state.name}",
  )

  self.logger.info("[DUT] Disconnect RFCOMM channel.")
  await dut_ref_dlc.close()

Tests one or two RFCOMM socket connections simultaneously.

Typical duration: 30-50s.

Test steps
  1. Create RFCOMM sockets server on REF.
  2. Pair DUT and REF.
  3. Create RFCOMM sockets connection from DUT to REF.
  4. Verify RFCOMM sockets connection are successful.

Parameters:

Name Type Description Default
num_connections int

The number of RFCOMM socket connections to create.

required
Source code in navi/tests/functionality/rfcomm_socket_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
@navi_test_base.parameterized(1, 2)
async def test_rfcomm_socket_connections_simultaneously(
    self, num_connections: int
) -> None:
  """Tests one or two RFCOMM socket connections simultaneously.

  Typical duration: 30-50s.

  Test steps:
    1. Create RFCOMM sockets server on REF.
    2. Pair DUT and REF.
    3. Create RFCOMM sockets connection from DUT to REF.
    4. Verify RFCOMM sockets connection are successful.

  Args:
    num_connections: The number of RFCOMM socket connections to create.
  """
  # Initialize RFCOMM sockets server on REF.
  rfcomm_server = rfcomm.Server(self.ref.device)

  # Create RFCOMM sockets server on REF.
  rfcomm_uuid_list = [str(uuid.uuid4()) for _ in range(num_connections)]
  ref_accept_queues = [
      self._setup_rfcomm_server_on_ref(i, rfcomm_uuid, rfcomm_server)
      for i, rfcomm_uuid in enumerate(rfcomm_uuid_list)
  ]

  # Pair DUT and REF.
  await self._setup_pairing()

  # Create RFCOMM sockets connection from DUT to REF.
  rfcomm_sockets = [
      self.dut.bl4a.create_rfcomm_channel_async(
          address=self.ref.address,
          secure=True,
          uuid=rfcomm_uuid,
      )
      for rfcomm_uuid in rfcomm_uuid_list
  ]

  async with self.assert_not_timeout(_DEFAULT_STEP_TIMEOUT_SECONDS):
    # Wait for all RFCOMM sockets connection to complete, and get the results.
    self.logger.info("[REF] Wait for all RFCOMM connections to be accepted.")
    server_accept_results = await asyncio.gather(
        *[q.get() for q in ref_accept_queues]
    )
    self.logger.info("[DUT] Wait for all RFCOMM connections to complete.")
    await asyncio.gather(*[
        rfcomm_socket.wait_for_connected() for rfcomm_socket in rfcomm_sockets
    ])
    self.logger.info("[DUT] All RFCOMM connections completed.")

    # Verify both RFCOMM sockets connection are successful.
    for dlc_result in server_accept_results:
      self.logger.info("dlc_result: %s", dlc_result)
      self.assertEqual(
          dlc_result.state,
          rfcomm.DLC.State.CONNECTED,
          "DLC connection failed. Expected state: CONNECTED, but got:"
          f" {dlc_result.state.name}",
      )

Bases: TwoDevicesTestBase

Tests of VAP (Voice Assistant Profile) server implementation.

Source code in navi/tests/functionality/vap_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
class VapTest(navi_test_base.TwoDevicesTestBase):
  """Tests of VAP (Voice Assistant Profile) server implementation."""

  ref_ascs: ascs.AudioStreamControlService

  def _setup_unicast_server(self) -> None:
    self.ref.device.add_service(pacs.make_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)

  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
      )
      await dut_lea_cb.wait_for_event(
          bl4a_api.ProfileActiveDeviceChanged(self.ref.random_address)
      )

  def _disable_voice_command_apps_except_snippet(self) -> None:
    # Disable all other voice command apps to prevent choosing activities.
    voice_command_packages: set[str] = set(
        re.findall(
            r"packageName=(.+)",
            self.dut.shell([
                "pm",
                "query-activities",
                "-a",
                _ACTION_VOICE_COMMAND,
            ]),
        )
    )

    def voice_command_package_callback(package: str) -> None:
      self.logger.info("[DUT] Re-Enable voice command app: %s.", package)
      self.dut.shell(["pm", "enable", package])

    for package in voice_command_packages:
      if package == android_constants.PACKAGE_NAME_BLUETOOTH_SNIPPET:
        continue
      self.logger.info("[DUT] Disable voice command app: %s.", package)
      self.dut.shell(["pm", "disable", package])
      self.test_class_context.callback(voice_command_package_callback, package)

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

    if self.dut.device.is_emulator:
      self.setprop_for_class_context(
          _AndroidProperty.VAP_SERVER_ENABLED, "true"
      )

    if not self.dut.is_le_audio_supported:
      raise signals.TestAbortClass("[DUT] Device does not support LE Audio.")

    if self.dut.getprop(_AndroidProperty.VAP_SERVER_ENABLED) != "true":
      raise signals.TestAbortClass("VAP server is not enabled")

    # Disable all other voice command apps to prevent choosing activities.
    self._disable_voice_command_apps_except_snippet()

    self.ref.config.cis_enabled = True
    self.ref.device.cis_enabled = True

    # b/480360111: Having some problems with EATT and VAP.
    self.ref.config.eatt_enabled = False

    self.setprop_for_class_context(
        _AndroidProperty.LEAUDIO_BYPASS_ALLOW_LIST, "true"
    )

  @override
  async def async_teardown_class(self) -> None:
    await super().async_teardown_class()
    self.dut.shell(["settings", "reset", "secure", "assistant"])

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

    self._setup_unicast_server()
    self.logger.info("[DUT] Open server.")
    await self._prepare_paired_devices()

    self.dut.shell(["settings", "put", "secure", "assistant", _ASSISTANT_NAME])

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

  async def _init_vas_client(
      self,
  ) -> vap.GenericVoiceAssistantServiceProxy:
    self.logger.info("[REF] Init VAS client.")
    ref_dut_acl = list(self.ref.device.connections.values())[0]
    async with bumble.device.Peer(ref_dut_acl) as peer:
      if not (
          vas_client := peer.create_service_proxy(
              vap.GenericVoiceAssistantServiceProxy
          )
      ):
        self.fail("VAS server not found.")
    await vas_client.subscribe_characteristics()
    return vas_client

  async def test_discover_and_read_vas_properties(self) -> None:
    """Tests VAP discovery and reading VAS properties.

    Test steps:
      1. Discover VAS Service and Characteristics.
      2. VAS Service and Characteristics should be discovered successfully.
      3. Read VA Name, VA UUID, VA CCID.
      4. VA name should not be None.
      5. VA UUID should be all zeros.
      6. CCID value read should be 3.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      vas_client = await self._init_vas_client()

      # VA Name
      self.logger.info("[REF] Read VA Name.")
      va_name = await vas_client.va_name.read_value()
      self.assertStartsWith(va_name, _ASSISTANT_NAME)

      # VA UUID
      self.logger.info("[REF] Read VA UUID.")
      va_uuid = await vas_client.va_uuid.read_value()
      self.assertStartsWith(va_uuid, _ASSISTANT_NAME.encode("utf-8"))

      # VA CCID
      self.logger.info("[REF] Read VA CCID Value.")
      self.assertNotEqual(await vas_client.va_ccid.read_value(), 0)

  async def test_invalid_opcode(self) -> None:
    """Tests Invalid opcode for VAP.

    Test steps:
      1. Discover VAP service.
      2. Read and subscribe to VAP characteristics.
      3. Write invalid opcode to VAP control point (other than 0x00, 0x01,
      0x02).
      4. Result should be opcode OPCODE_NOT_SUPPORTED.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      vas_client = await self._init_vas_client()
      self.logger.info("[REF] Init VAP Session.")
      self.assertEqual(
          await vas_client.write_control_point(
              vap.ControlPointOpcode.INVALID_OPCODE
          ),
          vap.ResponseCodeValue.OP_CODE_NOT_SUPPORTED,
      )

  async def test_stop_va_ready_state(self) -> None:
    """Tests Stop VAP when VA is ready.

    Test steps:
      1. Discover VAP service.
      2. Read and subscribe to VAP characteristics.
      3. Initialize VA.
      4. Stop VA
      5. Result should be opcode INVALID_SESSION_STATE.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      vas_client = await self._init_vas_client()
      self.logger.info("[REF] Init VAP Session.")
      self.assertEqual(
          await vas_client.initialize_va_session(),
          vap.ResponseCodeValue.SUCCESS,
      )

      self.logger.info("[REF] Stop VAP Session.")
      self.assertEqual(
          await vas_client.stop_va_session(),
          vap.ResponseCodeValue.INVALID_SESSION_STATE,
      )

  async def test_va_stop_without_va_initialize(self) -> None:
    """Tests Stop VAP when VA is ready.

    Test steps:
      1. Discover VAP service.
      2. Read and subscribe to VAP characteristics.
      3. Stop VA, ensure that the VA is not in VA_SESSION_ACTIVE state.
      4. Result should be INVALID_SESSION_STATE.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      vas_client = await self._init_vas_client()

      self.logger.info("[REF] Stop VAP Session.")
      self.assertEqual(
          await vas_client.stop_va_session(),
          vap.ResponseCodeValue.INVALID_SESSION_STATE,
      )

  async def test_va_start_without_va_initialize(self) -> None:
    """Tests VAP VA start from VAT.

    Test steps:
      1. Discover VAP service.
      2. Read and subscribe to VAP characteristics.
      3. Start VA.
      4. Result should be opcode INVALID_SESSION_STATE.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      vas_client = await self._init_vas_client()
      self.logger.info("[REF] Start VAP Service.")
      with self.assertRaises(vap.VapError) as e:
        await vas_client.start_va_session()
      self.assertEqual(
          e.exception.error_code, vap.ResponseCodeValue.INVALID_SESSION_STATE
      )

  async def test_start_and_stop_va_session(self) -> None:
    """Tests VAP VA start from VAT.

    Test steps:
      1. Discover VAP service.
      2. Read and subscribe to VAP characteristics.
      3. Initialize VA.
      4. Start VA.
      5. Stop VA.
    """
    async with self.assert_not_timeout(_TEST_TIMEOUT):
      voice_command_cb = self.test_case_context.enter_context(
          self.dut.bl4a.register_voice_command_callback()
      )
      vas_client = await self._init_vas_client()
      self.dut.bl4a.set_audio_attributes(
          bl4a_api.AudioAttributes(
              usage=bl4a_api.AudioAttributes.Usage.ASSISTANT
          ),
          handle_audio_focus=False,
      )

      self.logger.info("[REF] Init VAP Session.")
      self.assertEqual(
          await vas_client.initialize_va_session(),
          vap.ResponseCodeValue.SUCCESS,
      )

      self.logger.info("[REF] Start VAP Service.")
      start_task = asyncio.create_task(vas_client.start_va_session())

      self.logger.info("[DUT] Wait for Voice command.")
      await voice_command_cb.wait_for_event(
          bl4a_api.VoiceCommand(state=True),
          timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
      )
      self.logger.info("[DUT] Voice command received.")

      # Initialize Voice Recognition as audio source
      self.logger.info("[DUT] Start audio playback and recording.")
      audio_recorder = await asyncio.to_thread(
          lambda: self.dut.bl4a.start_audio_recording(
              _RECORDING_PATH,
              source=bl4a_api.AudioRecorder.Source.VOICE_RECOGNITION,
          )
      )
      self.test_case_context.push(audio_recorder)
      await asyncio.to_thread(self.dut.bt.audioPlaySine)

      self.logger.info("[REF] Wait for Start VA Session to complete.")
      await start_task

      self.logger.info("[REF] Verify Session State.")
      await vas_client.va_session_state.wait_for_target_value(
          bytes([vap.VaSessionState.VA_SESSION_ACTIVE])
      )

      self.logger.info("Streaming for %s seconds.", _STREAMING_TIME_SECONDS)
      await asyncio.sleep(_STREAMING_TIME_SECONDS)

      self.logger.info("[REF] Stop VAP Session.")
      await vas_client.stop_va_session()
      await vas_client.va_session_state.wait_for_target_value(
          bytes([vap.VaSessionState.VA_SESSION_READY])
      )

Tests VAP discovery and reading VAS properties.

Test steps
  1. Discover VAS Service and Characteristics.
  2. VAS Service and Characteristics should be discovered successfully.
  3. Read VA Name, VA UUID, VA CCID.
  4. VA name should not be None.
  5. VA UUID should be all zeros.
  6. CCID value read should be 3.
Source code in navi/tests/functionality/vap_test.py
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
async def test_discover_and_read_vas_properties(self) -> None:
  """Tests VAP discovery and reading VAS properties.

  Test steps:
    1. Discover VAS Service and Characteristics.
    2. VAS Service and Characteristics should be discovered successfully.
    3. Read VA Name, VA UUID, VA CCID.
    4. VA name should not be None.
    5. VA UUID should be all zeros.
    6. CCID value read should be 3.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    vas_client = await self._init_vas_client()

    # VA Name
    self.logger.info("[REF] Read VA Name.")
    va_name = await vas_client.va_name.read_value()
    self.assertStartsWith(va_name, _ASSISTANT_NAME)

    # VA UUID
    self.logger.info("[REF] Read VA UUID.")
    va_uuid = await vas_client.va_uuid.read_value()
    self.assertStartsWith(va_uuid, _ASSISTANT_NAME.encode("utf-8"))

    # VA CCID
    self.logger.info("[REF] Read VA CCID Value.")
    self.assertNotEqual(await vas_client.va_ccid.read_value(), 0)

Tests Invalid opcode for VAP.

Test steps
  1. Discover VAP service.
  2. Read and subscribe to VAP characteristics.
  3. Write invalid opcode to VAP control point (other than 0x00, 0x01, 0x02).
  4. Result should be opcode OPCODE_NOT_SUPPORTED.
Source code in navi/tests/functionality/vap_test.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
async def test_invalid_opcode(self) -> None:
  """Tests Invalid opcode for VAP.

  Test steps:
    1. Discover VAP service.
    2. Read and subscribe to VAP characteristics.
    3. Write invalid opcode to VAP control point (other than 0x00, 0x01,
    0x02).
    4. Result should be opcode OPCODE_NOT_SUPPORTED.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    vas_client = await self._init_vas_client()
    self.logger.info("[REF] Init VAP Session.")
    self.assertEqual(
        await vas_client.write_control_point(
            vap.ControlPointOpcode.INVALID_OPCODE
        ),
        vap.ResponseCodeValue.OP_CODE_NOT_SUPPORTED,
    )

Tests VAP VA start from VAT.

Test steps
  1. Discover VAP service.
  2. Read and subscribe to VAP characteristics.
  3. Initialize VA.
  4. Start VA.
  5. Stop VA.
Source code in navi/tests/functionality/vap_test.py
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
async def test_start_and_stop_va_session(self) -> None:
  """Tests VAP VA start from VAT.

  Test steps:
    1. Discover VAP service.
    2. Read and subscribe to VAP characteristics.
    3. Initialize VA.
    4. Start VA.
    5. Stop VA.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    voice_command_cb = self.test_case_context.enter_context(
        self.dut.bl4a.register_voice_command_callback()
    )
    vas_client = await self._init_vas_client()
    self.dut.bl4a.set_audio_attributes(
        bl4a_api.AudioAttributes(
            usage=bl4a_api.AudioAttributes.Usage.ASSISTANT
        ),
        handle_audio_focus=False,
    )

    self.logger.info("[REF] Init VAP Session.")
    self.assertEqual(
        await vas_client.initialize_va_session(),
        vap.ResponseCodeValue.SUCCESS,
    )

    self.logger.info("[REF] Start VAP Service.")
    start_task = asyncio.create_task(vas_client.start_va_session())

    self.logger.info("[DUT] Wait for Voice command.")
    await voice_command_cb.wait_for_event(
        bl4a_api.VoiceCommand(state=True),
        timeout=_DEFAULT_STEP_TIMEOUT_SECONDS,
    )
    self.logger.info("[DUT] Voice command received.")

    # Initialize Voice Recognition as audio source
    self.logger.info("[DUT] Start audio playback and recording.")
    audio_recorder = await asyncio.to_thread(
        lambda: self.dut.bl4a.start_audio_recording(
            _RECORDING_PATH,
            source=bl4a_api.AudioRecorder.Source.VOICE_RECOGNITION,
        )
    )
    self.test_case_context.push(audio_recorder)
    await asyncio.to_thread(self.dut.bt.audioPlaySine)

    self.logger.info("[REF] Wait for Start VA Session to complete.")
    await start_task

    self.logger.info("[REF] Verify Session State.")
    await vas_client.va_session_state.wait_for_target_value(
        bytes([vap.VaSessionState.VA_SESSION_ACTIVE])
    )

    self.logger.info("Streaming for %s seconds.", _STREAMING_TIME_SECONDS)
    await asyncio.sleep(_STREAMING_TIME_SECONDS)

    self.logger.info("[REF] Stop VAP Session.")
    await vas_client.stop_va_session()
    await vas_client.va_session_state.wait_for_target_value(
        bytes([vap.VaSessionState.VA_SESSION_READY])
    )

Tests Stop VAP when VA is ready.

Test steps
  1. Discover VAP service.
  2. Read and subscribe to VAP characteristics.
  3. Initialize VA.
  4. Stop VA
  5. Result should be opcode INVALID_SESSION_STATE.
Source code in navi/tests/functionality/vap_test.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
async def test_stop_va_ready_state(self) -> None:
  """Tests Stop VAP when VA is ready.

  Test steps:
    1. Discover VAP service.
    2. Read and subscribe to VAP characteristics.
    3. Initialize VA.
    4. Stop VA
    5. Result should be opcode INVALID_SESSION_STATE.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    vas_client = await self._init_vas_client()
    self.logger.info("[REF] Init VAP Session.")
    self.assertEqual(
        await vas_client.initialize_va_session(),
        vap.ResponseCodeValue.SUCCESS,
    )

    self.logger.info("[REF] Stop VAP Session.")
    self.assertEqual(
        await vas_client.stop_va_session(),
        vap.ResponseCodeValue.INVALID_SESSION_STATE,
    )

Tests VAP VA start from VAT.

Test steps
  1. Discover VAP service.
  2. Read and subscribe to VAP characteristics.
  3. Start VA.
  4. Result should be opcode INVALID_SESSION_STATE.
Source code in navi/tests/functionality/vap_test.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
async def test_va_start_without_va_initialize(self) -> None:
  """Tests VAP VA start from VAT.

  Test steps:
    1. Discover VAP service.
    2. Read and subscribe to VAP characteristics.
    3. Start VA.
    4. Result should be opcode INVALID_SESSION_STATE.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    vas_client = await self._init_vas_client()
    self.logger.info("[REF] Start VAP Service.")
    with self.assertRaises(vap.VapError) as e:
      await vas_client.start_va_session()
    self.assertEqual(
        e.exception.error_code, vap.ResponseCodeValue.INVALID_SESSION_STATE
    )

Tests Stop VAP when VA is ready.

Test steps
  1. Discover VAP service.
  2. Read and subscribe to VAP characteristics.
  3. Stop VA, ensure that the VA is not in VA_SESSION_ACTIVE state.
  4. Result should be INVALID_SESSION_STATE.
Source code in navi/tests/functionality/vap_test.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
async def test_va_stop_without_va_initialize(self) -> None:
  """Tests Stop VAP when VA is ready.

  Test steps:
    1. Discover VAP service.
    2. Read and subscribe to VAP characteristics.
    3. Stop VA, ensure that the VA is not in VA_SESSION_ACTIVE state.
    4. Result should be INVALID_SESSION_STATE.
  """
  async with self.assert_not_timeout(_TEST_TIMEOUT):
    vas_client = await self._init_vas_client()

    self.logger.info("[REF] Stop VAP Session.")
    self.assertEqual(
        await vas_client.stop_va_session(),
        vap.ResponseCodeValue.INVALID_SESSION_STATE,
    )

Bases: TwoDevicesTestBase

Tests for LE Audio Volume Control Profile.

Source code in navi/tests/functionality/vcp_test.py
 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
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
class VcpTest(navi_test_base.TwoDevicesTestBase):
  """Tests for LE Audio Volume Control Profile."""

  @override
  async def async_setup_class(self) -> None:
    await super().async_setup_class()
    if self.dut.device.is_emulator:
      # Force enable VCP controller and CSIP coordinator on the emulator.
      self.dut.shell(['setprop', _Property.VCP_CONTROLLER_ENABLED, 'true'])
      self.dut.shell(
          ['setprop', _Property.CSIP_SET_COORDINATOR_ENABLED, 'true']
      )
    if self.dut.getprop(_Property.VCP_CONTROLLER_ENABLED) != 'true':
      raise signals.TestAbortClass('VCP Controller is not enabled on DUT.')

    if not self.dut.is_le_audio_supported:
      raise signals.TestAbortClass('[DUT] Device does not support LE Audio.')

  async def _check_default_aics_properties(
      self, aics_cb: bl4a_api.AudioInputControl
  ) -> None:
    """Checks default AICS properties."""
    async with self.assert_not_timeout(_TIMEOUT):
      self.assertEqual(await aics_cb.get_description(), 'Bluetooth')
      self.assertEqual(await aics_cb.get_gain_setting_unit(), 1)
      self.assertEqual(await aics_cb.get_gain_setting_min(), 0)
      self.assertEqual(await aics_cb.get_gain_setting_max(), 127)
      self.assertEqual(
          await aics_cb.get_audio_input_status(),
          aics.AudioInputStatus.ACTIVE,
      )
      self.assertEqual(await aics_cb.get_gain_setting(), 0)
      self.assertEqual(await aics_cb.get_mute(), aics.Mute.NOT_MUTED)
      self.assertEqual(
          await aics_cb.get_gain_mode(),
          aics.GainMode.MANUAL,
      )

  async def _setup_writable_aics_and_connect(
      self,
  ) -> bl4a_api.AudioInputControl:
    """Sets up VCS with one writable AICS and connects."""
    aics_service = aics_ext.AudioInputControlService(
        audio_input_description=aics.AudioInputDescription(
            audio_input_description='Bluetooth'
        )
    )

    volume_control_service = vcs.VolumeControlService(
        included_services=[aics_service],
        volume_flags=vcs.VolumeFlags.VOLUME_SETTING_PERSISTED,
    )
    sirk = secrets.token_bytes(csip.SET_IDENTITY_RESOLVING_KEY_LENGTH)
    self.ref.device.add_services([
        volume_control_service,
        cap.CommonAudioServiceService(
            csip.CoordinatedSetIdentificationService(
                set_identity_resolving_key=sirk,
                set_identity_resolving_key_type=csip.SirkType.PLAINTEXT,
                coordinated_set_size=1,
            )
        ),
    ])

    self.logger.info('[DUT] Create bond with REF')
    with self.dut.bl4a.register_callback(
        bl4a_api.Module.VOLUME_CONTROL
    ) as vcp_cb:
      self.logger.info('[DUT] Setting VCP connection policy...')
      await self.le_connect_and_pair(hci.OwnAddressType.RANDOM, self.ref)
      self.dut.bt.vcpSetConnectionPolicy(
          self.ref.random_address, android_constants.ConnectionPolicy.ALLOWED
      )
      self.logger.info('[DUT] Waiting for VCP connection...')
      await vcp_cb.wait_for_event(
          bl4a_api.ProfileConnectionStateChanged(
              address=self.ref.random_address,
              state=android_constants.ConnectionState.CONNECTED,
          )
      )

    self.logger.info('[DUT] Getting AICS...')
    aics_cb = self.dut.bl4a.get_aics(self.ref.random_address, 0)
    self.test_case_context.enter_context(aics_cb)
    self.logger.info('[DUT] Waiting for AICS properties to be ready...')
    await aics_cb.wait_for_event(
        bl4a_api.AicsGainSettingChanged(gain_setting=0)
    )
    await aics_cb.wait_for_event(
        bl4a_api.AicsMuteChanged(mute=aics.Mute.NOT_MUTED)
    )
    await aics_cb.wait_for_event(
        bl4a_api.AicsGainModeChanged(gain_mode=aics.GainMode.MANUAL)
    )
    await aics_cb.wait_for_event(
        bl4a_api.AicsDescriptionChanged(description='Bluetooth')
    )
    self.logger.info('[DUT] Checking default AICS properties...')
    await self._check_default_aics_properties(aics_cb)
    return aics_cb

  async def test_aics_set_description(self) -> None:
    """Tests that AICS description can be set."""
    aics_cb = await self._setup_writable_aics_and_connect()
    async with self.assert_not_timeout(_TIMEOUT):
      is_writable = await aics_cb.is_description_writable()
      self.logger.info('is_writable: %s', is_writable)
      self.assertTrue(is_writable)

      self.assertTrue(await aics_cb.set_description('New Description'))
      await aics_cb.wait_for_event(
          bl4a_api.AicsDescriptionChanged(description='New Description')
      )
      self.assertEqual(await aics_cb.get_description(), 'New Description')

  async def test_aics_set_gain_setting(self) -> None:
    """Tests that AICS gain setting can be set."""
    aics_cb = await self._setup_writable_aics_and_connect()

    async with self.assert_not_timeout(_TIMEOUT):
      self.assertTrue(await aics_cb.set_gain_setting(100))
      await aics_cb.wait_for_event(
          bl4a_api.AicsGainSettingChanged(gain_setting=100)
      )
      self.assertEqual(await aics_cb.get_gain_setting(), 100)

  async def test_aics_set_mute(self) -> None:
    """Tests that AICS mute state can be set."""
    aics_cb = await self._setup_writable_aics_and_connect()

    async with self.assert_not_timeout(_TIMEOUT):
      self.assertTrue(await aics_cb.set_mute(aics.Mute.MUTED))
      await aics_cb.wait_for_event(
          bl4a_api.AicsMuteChanged(mute=aics.Mute.MUTED)
      )
      self.assertEqual(await aics_cb.get_mute(), aics.Mute.MUTED)

  async def test_aics_set_gain_mode(self) -> None:
    """Tests that AICS gain mode can be set."""
    aics_cb = await self._setup_writable_aics_and_connect()

    async with self.assert_not_timeout(_TIMEOUT):
      self.assertTrue(await aics_cb.set_gain_mode(aics.GainMode.AUTOMATIC))
      await aics_cb.wait_for_event(
          bl4a_api.AicsGainModeChanged(gain_mode=aics.GainMode.AUTOMATIC)
      )
      self.assertEqual(
          await aics_cb.get_gain_mode(),
          aics.GainMode.AUTOMATIC,
      )

  async def _setup_vocs_and_connect(
      self,
  ) -> bl4a_api.CallbackHandler:
    """Sets up VCS with one VOCS and connects."""
    volume_control_service = vcs.VolumeControlService(
        included_services=[
            vocs.VolumeOffsetControlService(
                change_counter=0,
                volume_offset=0,
                audio_location=bap.AudioLocation.FRONT_LEFT,
            )
        ]
    )
    sirk = secrets.token_bytes(csip.SET_IDENTITY_RESOLVING_KEY_LENGTH)
    self.ref.device.add_services([
        volume_control_service,
        cap.CommonAudioServiceService(
            csip.CoordinatedSetIdentificationService(
                set_identity_resolving_key=sirk,
                set_identity_resolving_key_type=csip.SirkType.PLAINTEXT,
                coordinated_set_size=1,
            )
        ),
    ])

    self.logger.info('[DUT] Create bond with REF')
    vcp_cb = self.dut.bl4a.register_callback(bl4a_api.Module.VOLUME_CONTROL)
    self.test_case_context.callback(vcp_cb.close)
    await self.le_connect_and_pair(hci.OwnAddressType.RANDOM, self.ref)
    self.logger.info('[DUT] Setting VCP connection policy...')
    self.dut.bt.vcpSetConnectionPolicy(
        self.ref.random_address, android_constants.ConnectionPolicy.ALLOWED
    )
    self.logger.info('[DUT] Waiting for VCP connection...')
    await vcp_cb.wait_for_event(
        bl4a_api.ProfileConnectionStateChanged(
            address=self.ref.random_address,
            state=android_constants.ConnectionState.CONNECTED,
        )
    )

    self.logger.info('[DUT] Getting VOCS...')

    self.logger.info('[DUT] Waiting for VOCS properties to be ready...')
    await vcp_cb.wait_for_event(
        bl4a_api.VocsOffsetStateChanged(
            address=self.ref.random_address,
            instance_id=1,
            offset=0,
        )
    )
    await vcp_cb.wait_for_event(
        bl4a_api.VocsAudioLocationChanged(
            address=self.ref.random_address,
            instance_id=1,
            audio_location=int(bap.AudioLocation.FRONT_LEFT),
        )
    )
    self.logger.info('[DUT] VOCS is ready.')
    return vcp_cb

  async def test_vocs_set_volume_offset(self) -> None:
    """Tests that VOCS volume offset can be set."""
    with await self._setup_vocs_and_connect() as vcp_cb:
      self.assertTrue(
          self.dut.bt.isVolumeOffsetAvailable(self.ref.random_address)
      )
      self.assertEqual(
          self.dut.bt.getNumberofVocsInstances(self.ref.random_address),
          1,
      )

      async with self.assert_not_timeout(_TIMEOUT):
        await asyncio.to_thread(
            self.dut.bt.setVolumeOffset, self.ref.random_address, 1, 100
        )
        self.logger.info('[DUT] Waiting for VOCS offset to be changed...')
        # we are getting the offset 256 times the value we pass
        await vcp_cb.wait_for_event(
            event=bl4a_api.VocsOffsetStateChanged(
                address=self.ref.random_address,
                instance_id=1,
                offset=25600,
            )
        )

  async def test_vocs_set_device_volume(self) -> None:
    """Tests that VOCS device volume can be set."""
    with await self._setup_vocs_and_connect() as vcp_cb:
      self.assertTrue(
          self.dut.bt.isVolumeOffsetAvailable(self.ref.random_address)
      )
      self.assertEqual(
          self.dut.bt.getNumberofVocsInstances(self.ref.random_address),
          1,
      )
      async with self.assert_not_timeout(_TIMEOUT):
        await asyncio.to_thread(
            self.dut.bt.vcpSetDeviceVolume, self.ref.random_address, 100, True
        )
        await vcp_cb.wait_for_event(
            bl4a_api.DeviceVolumeChanged(
                address=self.ref.random_address,
                volume=100,
            )
        )

Tests that AICS description can be set.

Source code in navi/tests/functionality/vcp_test.py
141
142
143
144
145
146
147
148
149
150
151
152
153
async def test_aics_set_description(self) -> None:
  """Tests that AICS description can be set."""
  aics_cb = await self._setup_writable_aics_and_connect()
  async with self.assert_not_timeout(_TIMEOUT):
    is_writable = await aics_cb.is_description_writable()
    self.logger.info('is_writable: %s', is_writable)
    self.assertTrue(is_writable)

    self.assertTrue(await aics_cb.set_description('New Description'))
    await aics_cb.wait_for_event(
        bl4a_api.AicsDescriptionChanged(description='New Description')
    )
    self.assertEqual(await aics_cb.get_description(), 'New Description')

Tests that AICS gain mode can be set.

Source code in navi/tests/functionality/vcp_test.py
177
178
179
180
181
182
183
184
185
186
187
188
189
async def test_aics_set_gain_mode(self) -> None:
  """Tests that AICS gain mode can be set."""
  aics_cb = await self._setup_writable_aics_and_connect()

  async with self.assert_not_timeout(_TIMEOUT):
    self.assertTrue(await aics_cb.set_gain_mode(aics.GainMode.AUTOMATIC))
    await aics_cb.wait_for_event(
        bl4a_api.AicsGainModeChanged(gain_mode=aics.GainMode.AUTOMATIC)
    )
    self.assertEqual(
        await aics_cb.get_gain_mode(),
        aics.GainMode.AUTOMATIC,
    )

Tests that AICS gain setting can be set.

Source code in navi/tests/functionality/vcp_test.py
155
156
157
158
159
160
161
162
163
164
async def test_aics_set_gain_setting(self) -> None:
  """Tests that AICS gain setting can be set."""
  aics_cb = await self._setup_writable_aics_and_connect()

  async with self.assert_not_timeout(_TIMEOUT):
    self.assertTrue(await aics_cb.set_gain_setting(100))
    await aics_cb.wait_for_event(
        bl4a_api.AicsGainSettingChanged(gain_setting=100)
    )
    self.assertEqual(await aics_cb.get_gain_setting(), 100)

Tests that AICS mute state can be set.

Source code in navi/tests/functionality/vcp_test.py
166
167
168
169
170
171
172
173
174
175
async def test_aics_set_mute(self) -> None:
  """Tests that AICS mute state can be set."""
  aics_cb = await self._setup_writable_aics_and_connect()

  async with self.assert_not_timeout(_TIMEOUT):
    self.assertTrue(await aics_cb.set_mute(aics.Mute.MUTED))
    await aics_cb.wait_for_event(
        bl4a_api.AicsMuteChanged(mute=aics.Mute.MUTED)
    )
    self.assertEqual(await aics_cb.get_mute(), aics.Mute.MUTED)

Tests that VOCS device volume can be set.

Source code in navi/tests/functionality/vcp_test.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
async def test_vocs_set_device_volume(self) -> None:
  """Tests that VOCS device volume can be set."""
  with await self._setup_vocs_and_connect() as vcp_cb:
    self.assertTrue(
        self.dut.bt.isVolumeOffsetAvailable(self.ref.random_address)
    )
    self.assertEqual(
        self.dut.bt.getNumberofVocsInstances(self.ref.random_address),
        1,
    )
    async with self.assert_not_timeout(_TIMEOUT):
      await asyncio.to_thread(
          self.dut.bt.vcpSetDeviceVolume, self.ref.random_address, 100, True
      )
      await vcp_cb.wait_for_event(
          bl4a_api.DeviceVolumeChanged(
              address=self.ref.random_address,
              volume=100,
          )
      )

Tests that VOCS volume offset can be set.

Source code in navi/tests/functionality/vcp_test.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
async def test_vocs_set_volume_offset(self) -> None:
  """Tests that VOCS volume offset can be set."""
  with await self._setup_vocs_and_connect() as vcp_cb:
    self.assertTrue(
        self.dut.bt.isVolumeOffsetAvailable(self.ref.random_address)
    )
    self.assertEqual(
        self.dut.bt.getNumberofVocsInstances(self.ref.random_address),
        1,
    )

    async with self.assert_not_timeout(_TIMEOUT):
      await asyncio.to_thread(
          self.dut.bt.setVolumeOffset, self.ref.random_address, 1, 100
      )
      self.logger.info('[DUT] Waiting for VOCS offset to be changed...')
      # we are getting the offset 256 times the value we pass
      await vcp_cb.wait_for_event(
          event=bl4a_api.VocsOffsetStateChanged(
              address=self.ref.random_address,
              instance_id=1,
              offset=25600,
          )
      )