Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 976e404c authored by Łukasz Rymanowski's avatar Łukasz Rymanowski
Browse files

state_machine: Fix race on switching buds

With this patch we handle following case.
0. Group contains Device A and Device B
1. Group is streaming only to Device A (B is OFF)
2. Device B reconnects to the phone and attaches to the stream
   (Configuration has been started)
3. In the mean time, Device A disconnects.

With this patch, Device B will continue with configuration dispite
the Device A which just disconnects.

Bug: 333970914
Test: atest bluetooth_le_audio_test
Flag: EXEMPT, regression verified with unit tests, new test added
Change-Id: If0cf374601b787431782b93fd9c5600087067aab
parent 34195ca0
Loading
Loading
Loading
Loading
+77 −33
Original line number Diff line number Diff line
@@ -1149,12 +1149,12 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
      if (ases_pair.sink &&
          ases_pair.sink->state == AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
        SetAseState(leAudioDevice, ases_pair.sink,
                    AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
                    AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
      }
      if (ases_pair.source && ases_pair.source->state ==
                                  AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
        SetAseState(leAudioDevice, ases_pair.source,
                    AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED);
                    AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
      }
    }

@@ -1162,7 +1162,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {

    auto target_state = group->GetTargetState();
    switch (target_state) {
      case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING:
      case AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING: {
        /* Something wrong happen when streaming or when creating stream.
         * If there is other device connected and streaming, just leave it as it
         * is, otherwise stop the stream.
@@ -1175,6 +1175,29 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
          return;
        }

        /* CISes are disconnected, but it could be a case here, that there is
         * another set member trying to get STREAMING state. Can happen when
         * while streaming user switch buds. In such a case, lets try to allow
         * that device to continue
         */

        LeAudioDevice* attaching_device =
            getDeviceTryingToAttachTheStream(group);
        if (attaching_device != nullptr) {
          /* There is a device willitng to stream. Let's wait for it to start
           * streaming */
          auto active_ase = attaching_device->GetFirstActiveAse();
          group->SetState(active_ase->state);

          /* this is just to start timer */
          group->SetTargetState(AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
          log::info(
              "{} is still attaching to stream while other members got "
              "disconnected from the group_id: {}",
              attaching_device->address_, group->group_id_);
          return;
        }

        log::info("Lost all members from the group {}", group->group_id_);
        group->cig.cises.clear();
        RemoveCigForGroup(group);
@@ -1185,6 +1208,7 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
        state_machine_callbacks_->StatusReportCb(group->group_id_,
                                                 GroupStreamStatus::IDLE);
        return;
      }

      case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
        /* Intentional group disconnect has finished, but the last CIS in the
@@ -1769,6 +1793,29 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
    ase->state = state;
  }

  LeAudioDevice* getDeviceTryingToAttachTheStream(LeAudioDeviceGroup* group) {
    /* Device which is attaching the stream is just an active device not in
     * STREAMING state. the precondition is, that TargetState is Streaming  */

    log::debug("group_id: {}, targetState: {}", group->group_id_,
               ToString(group->GetTargetState()));

    if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
      return nullptr;
    }

    for (auto dev = group->GetFirstActiveDevice(); dev != nullptr;
         dev = group->GetNextActiveDevice(dev)) {
      if (!dev->HaveAllActiveAsesSameState(
              AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING)) {
        log::debug("Attaching device {} to group_id: {}", dev->address_,
                   group->group_id_);
        return dev;
      }
    }
    return nullptr;
  }

  void AseStateMachineProcessIdle(
      struct bluetooth::le_audio::client_parser::ascs::ase_rsp_hdr& arh,
      struct ase* ase, LeAudioDeviceGroup* group,
@@ -1792,15 +1839,6 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
          return;
        }

        /* Before continue with release, make sure this is what is requested.
         * If not (e.g. only single device got disconnected), stop here
         */
        if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
          log::debug("Autonomus change of stated for device {}, ase id: {}",
                     leAudioDevice->address_, ase->id);
          return;
        }

        if (!group->HaveAllActiveDevicesAsesTheSameState(
                AseState::BTA_LE_AUDIO_ASE_STATE_IDLE)) {
          log::debug("Waiting for more devices to get into idle state");
@@ -1813,7 +1851,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {

        /* If all CISes are disconnected, notify upper layer about IDLE state,
         * otherwise wait for */
        if (!group->HaveAllCisesDisconnected()) {
        if (!group->HaveAllCisesDisconnected() ||
            getDeviceTryingToAttachTheStream(group) != nullptr) {
          log::warn(
              "Not all CISes removed before going to IDLE for group {}, "
              "waiting...",
@@ -2079,7 +2118,12 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {

        if (group->GetTargetState() ==
            AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
          if (!CigCreate(group)) {
          if (group->cig.GetState() == CigState::CREATED) {
            /* It can happen on the earbuds switch scenario. When one device
             * is getting remove while other is adding to the stream and CIG is
             * already created */
            PrepareAndSendConfigQos(group, leAudioDevice);
          } else if (!CigCreate(group)) {
            log::error("Could not create CIG. Stop the stream for group {}",
                       group->group_id_);
            StopStream(group);
@@ -2174,7 +2218,12 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {

        if (group->GetTargetState() ==
            AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING) {
          if (!CigCreate(group)) {
          if (group->cig.GetState() == CigState::CREATED) {
            /* It can happen on the earbuds switch scenario. When one device
             * is getting remove while other is adding to the stream and CIG is
             * already created */
            PrepareAndSendConfigQos(group, leAudioDevice);
          } else if (!CigCreate(group)) {
            log::error("Could not create CIG. Stop the stream for group {}",
                       group->group_id_);
            StopStream(group);
@@ -2229,15 +2278,6 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
          return;
        }

        /* Before continue with release, make sure this is what is requested.
         * If not (e.g. only single device got disconnected), stop here
         */
        if (group->GetTargetState() != AseState::BTA_LE_AUDIO_ASE_STATE_IDLE) {
          log::debug("Autonomus change of stated for device {}, ase id: {}",
                     leAudioDevice->address_, ase->id);
          return;
        }

        {
          auto activeDevice = group->GetFirstActiveDevice();
          if (activeDevice) {
@@ -2296,6 +2336,17 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
    }

    switch (ase->state) {
      case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
        log::info(
            "Unexpected state transition from {} to {}, {}, ase_id: {}, "
            "fallback to transition from {} to {}",
            ToString(ase->state),
            ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED),
            leAudioDevice->address_, ase->id,
            ToString(AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED),
            ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED));
        group->PrintDebugState();
        FMT_FALLTHROUGH;
      case AseState::BTA_LE_AUDIO_ASE_STATE_CODEC_CONFIGURED: {
        SetAseState(leAudioDevice, ase,
                    AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED);
@@ -2379,14 +2430,6 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
        }
        break;
      }

      case AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED:
        log::info("Unexpected state transition from {} to {}, {}, ase_id: {}",
                  ToString(ase->state),
                  ToString(AseState::BTA_LE_AUDIO_ASE_STATE_QOS_CONFIGURED),
                  leAudioDevice->address_, ase->id);
        group->PrintDebugState();
        break;
      case AseState::BTA_LE_AUDIO_ASE_STATE_IDLE:
      case AseState::BTA_LE_AUDIO_ASE_STATE_RELEASING:
        // Do nothing here, just print an error message
@@ -3067,7 +3110,8 @@ class LeAudioGroupStateMachineImpl : public LeAudioGroupStateMachine {
        }

        if (group->cig.GetState() == CigState::CREATED &&
            group->HaveAllCisesDisconnected()) {
            group->HaveAllCisesDisconnected() &&
            getDeviceTryingToAttachTheStream(group) == nullptr) {
          RemoveCigForGroup(group);
        }

+188 −9
Original line number Diff line number Diff line
@@ -747,6 +747,35 @@ class StateMachineTestBase : public Test {
        group, leAudioDevice);
  }

  void InjectReleasingAndIdleState(LeAudioDeviceGroup* group,
                                   LeAudioDevice* device) {
    for (auto& ase : device->ases_) {
      if (ase.id == bluetooth::le_audio::types::ase::kAseIdInvalid) {
        continue;
      }
      // Simulate autonomus RELEASE and moving to IDLE state
      InjectAseStateNotification(&ase, device, group, ascs::kAseStateReleasing,
                                 nullptr);
      InjectAseStateNotification(&ase, device, group, ascs::kAseStateIdle,
                                 nullptr);
    }
  }

  void InjectCachedConfiguratuibForActiveAses(LeAudioDeviceGroup* group,
                                              LeAudioDevice* device) {
    for (auto& ase : device->ases_) {
      if (!ase.active) {
        continue;
      }
      log::info("ID : {},  status {}", ase.id,
                bluetooth::common::ToString(ase.state));

      InjectAseStateNotification(&ase, device, group,
                                 ascs::kAseStateCodecConfigured,
                                 &cached_codec_configuration_map_[ase.id]);
    }
  }

  void InjectAseStateNotification(types::ase* ase, LeAudioDevice* device,
                                  LeAudioDeviceGroup* group, uint8_t new_state,
                                  void* new_state_params) {
@@ -1109,10 +1138,12 @@ class StateMachineTestBase : public Test {

  void PrepareConfigureCodecHandler(LeAudioDeviceGroup* group,
                                    int verify_ase_count = 0,
                                    bool caching = false) {
                                    bool caching = false,
                                    bool inject_configured = true) {
    ON_CALL(ase_ctp_handler, AseCtpConfigureCodecHandler)
        .WillByDefault(Invoke([group, verify_ase_count, caching, this](
                                  LeAudioDevice* device,
        .WillByDefault(Invoke([group, verify_ase_count, caching,
                               inject_configured,
                               this](LeAudioDevice* device,
                                     std::vector<uint8_t> value,
                                     GATT_WRITE_OP_CB cb, void* cb_data) {
          auto num_ase = value[1];
@@ -1169,9 +1200,12 @@ class StateMachineTestBase : public Test {
              cached_codec_configuration_map_[ase_id] =
                  codec_configured_state_params;
            }

            if (inject_configured) {
              InjectAseStateNotification(ase, device, group,
                                         ascs::kAseStateCodecConfigured,
                                         &codec_configured_state_params);
            }

            if (stop_inject_configured_ase_after_first_ase_configured_) {
              return;
@@ -6245,7 +6279,7 @@ TEST_F(StateMachineTest, StartStreamCachedConfigReconfigInvalidBehavior) {
  for (auto& ase : device->ases_) {
    if (i++ == 0) continue;

    // Simulate autonomus release for one ASE - this is invalid behaviour
    // Simulate autonomus release for one ASE
    InjectAseStateNotification(&ase, device, group, ascs::kAseStateReleasing,
                               nullptr);
  }
@@ -7444,6 +7478,151 @@ TEST_F(StateMachineTest, testAttachDeviceToTheStreamCisFailure) {
  ASSERT_NE(ase->qos_config.retrans_nb, 0);
}

TEST_F(StateMachineTest, testAttachDeviceWhileSecondDeviceDisconnects) {
  const auto context_type = kContextTypeMedia;
  const auto leaudio_group_id = 6;
  const auto num_devices = 2;

  ContentControlIdKeeper::GetInstance()->SetCcid(media_context, media_ccid);

  // Prepare multiple fake connected devices in a group
  auto* group =
      PrepareSingleTestDeviceGroup(leaudio_group_id, context_type, num_devices);
  ASSERT_EQ(group->Size(), num_devices);

  PrepareConfigureCodecHandler(group);
  PrepareConfigureQosHandler(group);
  PrepareEnableHandler(group);
  PrepareDisableHandler(group);
  PrepareReleaseHandler(group);

  auto* leAudioDevice = group->GetFirstDevice();
  LeAudioDevice* lastDevice;
  LeAudioDevice* firstDevice = leAudioDevice;

  auto expected_devices_written = 0;
  while (leAudioDevice) {
    /* Three Writes:
     * 1: Codec Config
     * 2: Codec QoS
     * 3: Enable
     */
    lastDevice = leAudioDevice;
    EXPECT_CALL(gatt_queue,
                WriteCharacteristic(leAudioDevice->conn_id_,
                                    leAudioDevice->ctp_hdls_.val_hdl, _,
                                    GATT_WRITE_NO_RSP, _, _))
        .Times(AtLeast(3));
    expected_devices_written++;
    leAudioDevice = group->GetNextDevice(leAudioDevice);
  }
  ASSERT_EQ(expected_devices_written, num_devices);

  EXPECT_CALL(*mock_iso_manager_, CreateCig(_, _)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(2);

  InjectInitialIdleNotification(group);

  // Start the configuration and stream Media content
  LeAudioGroupStateMachine::Get()->StartStream(
      group, context_type,
      {.sink = types::AudioContexts(context_type),
       .source = types::AudioContexts(context_type)});

  // Check if group has transitioned to a proper state
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);
  testing::Mock::VerifyAndClearExpectations(mock_iso_manager_);

  // Inject CIS and ACL disconnection of first device
  InjectCisDisconnected(group, lastDevice, HCI_ERR_CONNECTION_TOUT);
  InjectAclDisconnected(group, lastDevice);

  log::info(" Device B - Disconnected ");

  // Check if group keeps streaming
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);

  // Set second device is connected now.
  lastDevice->conn_id_ = 3;
  lastDevice->SetConnectionState(DeviceConnectState::CONNECTED);

  // Make sure ASE with disconnected CIS are not left in STREAMING
  ASSERT_EQ(lastDevice->GetFirstAseWithState(
                ::bluetooth::le_audio::types::kLeAudioDirectionSink,
                types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING),
            nullptr);
  ASSERT_EQ(lastDevice->GetFirstAseWithState(
                ::bluetooth::le_audio::types::kLeAudioDirectionSource,
                types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING),
            nullptr);

  // Expect just Codec Configure on ASCS Control Point
  EXPECT_CALL(gatt_queue, WriteCharacteristic(lastDevice->conn_id_,
                                              lastDevice->ctp_hdls_.val_hdl, _,
                                              GATT_WRITE_NO_RSP, _, _))
      .Times(AtLeast(1));

  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(0);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(0);

  // Remove Configuration incjection but cache configuration for future
  // injection
  PrepareConfigureCodecHandler(group, 0, true, false);

  log::info("Device B - Attaching to the stream");

  LeAudioGroupStateMachine::Get()->AttachToStream(
      group, lastDevice, {.sink = {media_ccid}, .source = {}});

  // Check if group keeps streaming
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);

  /* Verify that ASE of first device are still good*/
  auto ase = firstDevice->GetFirstActiveAse();
  ASSERT_NE(ase->qos_config.max_transport_latency, 0);
  ASSERT_NE(ase->qos_config.retrans_nb, 0);

  testing::Mock::VerifyAndClearExpectations(mock_iso_manager_);
  testing::Mock::VerifyAndClearExpectations(&gatt_queue);

  log::info(
      "Device A is disconnecting while Device B is attaching to the stream");

  InjectCisDisconnected(group, firstDevice, HCI_ERR_CONNECTION_TOUT);
  InjectReleasingAndIdleState(group, firstDevice);

  // Check if group keeps streaming
  ASSERT_EQ(group->GetState(), types::AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
  ASSERT_EQ(group->GetTargetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);

  ASSERT_EQ(group->cig.GetState(), types::CigState::CREATED);

  log::info("Device B continues configuration and streaming");

  // Expect QoS config and Enable on ASCS Control Point
  EXPECT_CALL(gatt_queue, WriteCharacteristic(lastDevice->conn_id_,
                                              lastDevice->ctp_hdls_.val_hdl, _,
                                              GATT_WRITE_NO_RSP, _, _))
      .Times(AtLeast(2));

  EXPECT_CALL(*mock_iso_manager_, EstablishCis(_)).Times(1);
  EXPECT_CALL(*mock_iso_manager_, SetupIsoDataPath(_, _)).Times(1);

  InjectCachedConfiguratuibForActiveAses(group, lastDevice);

  // Check if group keeps streaming
  ASSERT_EQ(group->GetState(),
            types::AseState::BTA_LE_AUDIO_ASE_STATE_STREAMING);

  testing::Mock::VerifyAndClearExpectations(mock_iso_manager_);
  testing::Mock::VerifyAndClearExpectations(&gatt_queue);
}

TEST_F(StateMachineTest, testAclDropWithoutApriorCisDisconnection) {
  const auto context_type = kContextTypeMedia;
  const auto leaudio_group_id = 6;